Php4ConstructorFixer.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <?php
  2. /*
  3. * This file is part of the PHP CS utility.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace Symfony\CS\Fixer\Contrib;
  11. use Symfony\CS\AbstractFixer;
  12. use Symfony\CS\Tokenizer\Token;
  13. use Symfony\CS\Tokenizer\Tokens;
  14. /**
  15. * @author Matteo Beccati <matteo@beccati.com>
  16. */
  17. class Php4ConstructorFixer extends AbstractFixer
  18. {
  19. /**
  20. * {@inheritdoc}
  21. */
  22. public function fix(\SplFileInfo $file, Tokens $tokens)
  23. {
  24. $classes = array_keys($tokens->findGivenKind(T_CLASS));
  25. $numClasses = count($classes);
  26. for ($i = 0; $i < $numClasses; ++$i) {
  27. $index = $classes[$i];
  28. // is it inside a namespace?
  29. $nspIndex = $tokens->getPrevTokenOfKind($index, array(array(T_NAMESPACE, 'namespace')));
  30. if (null !== $nspIndex) {
  31. $nspIndex = $tokens->getNextMeaningfulToken($nspIndex);
  32. // make sure it's not the global namespace, as PHP4 constructors are allowed in there
  33. if (!$tokens[$nspIndex]->equals('{')) {
  34. // unless it's the global namespace, the index currently points to the name
  35. $nspIndex = $tokens->getNextMeaningfulToken($nspIndex);
  36. if ($tokens[$nspIndex]->equals(';')) {
  37. // the class is inside a (non-block) namespace, no PHP4-code should be in there
  38. break;
  39. }
  40. // the index points to the { of a block-namespace
  41. $nspEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nspIndex);
  42. if ($index < $nspEnd) {
  43. // the class is inside a block namespace, skip other classes that might be in it
  44. for ($j = $i + 1; $j < $numClasses; ++$j) {
  45. if ($classes[$j] < $nspEnd) {
  46. ++$i;
  47. }
  48. }
  49. // and continue checking the classes that might follow
  50. continue;
  51. }
  52. }
  53. }
  54. $classNameIndex = $tokens->getNextMeaningfulToken($index);
  55. $className = $tokens[$classNameIndex]->getContent();
  56. $classStart = $tokens->getNextTokenOfKind($classNameIndex, array('{'));
  57. $classEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classStart);
  58. $this->fixConstructor($tokens, $className, $classStart, $classEnd);
  59. $this->fixParent($tokens, $classStart, $classEnd);
  60. }
  61. }
  62. /**
  63. * {@inheritdoc}
  64. */
  65. public function getName()
  66. {
  67. return 'php4_constructor';
  68. }
  69. /**
  70. * {@inheritdoc}
  71. */
  72. public function getDescription()
  73. {
  74. return 'Convert PHP4-style constructors to __construct. Warning! This could change code behavior.';
  75. }
  76. /**
  77. * Fix constructor within a class, if possible.
  78. *
  79. * @param Tokens $tokens the Tokens instance
  80. * @param string $className the class name
  81. * @param int $classStart the class start index
  82. * @param int $classEnd the class end index
  83. */
  84. private function fixConstructor(Tokens $tokens, $className, $classStart, $classEnd)
  85. {
  86. $php4 = $this->findFunction($tokens, $className, $classStart, $classEnd);
  87. if (null === $php4) {
  88. // no PHP4-constructor!
  89. return;
  90. }
  91. if (!empty($php4['modifiers'][T_ABSTRACT]) || !empty($php4['modifiers'][T_STATIC])) {
  92. // PHP4 constructor can't be abstract or static
  93. return;
  94. }
  95. $php5 = $this->findFunction($tokens, '__construct', $classStart, $classEnd);
  96. if (null === $php5) {
  97. // no PHP5-constructor, we can rename the old one to __construct
  98. $tokens[$php4['nameIndex']]->setContent('__construct');
  99. return;
  100. }
  101. // does the PHP4-constructor only call $this->__construct($args, ...)?
  102. list($seq, $case) = $this->getWrapperMethodSequence($tokens, '__construct', $php4['startIndex'], $php4['bodyIndex']);
  103. if (null !== $tokens->findSequence($seq, $php4['bodyIndex'] - 1, $php4['endIndex'], $case)) {
  104. // good, delete it!
  105. for ($i = $php4['startIndex']; $i <= $php4['endIndex']; ++$i) {
  106. $tokens[$i]->clear();
  107. }
  108. return;
  109. }
  110. // does __construct only call the PHP4-constructor (with the same args)?
  111. list($seq, $case) = $this->getWrapperMethodSequence($tokens, $className, $php4['startIndex'], $php4['bodyIndex']);
  112. if (null !== $tokens->findSequence($seq, $php5['bodyIndex'] - 1, $php5['endIndex'], $case)) {
  113. // that was a weird choice, but we can safely delete it and...
  114. for ($i = $php5['startIndex']; $i <= $php5['endIndex']; ++$i) {
  115. $tokens[$i]->clear();
  116. }
  117. // rename the PHP4 one to __construct
  118. $tokens[$php4['nameIndex']]->setContent('__construct');
  119. }
  120. }
  121. /**
  122. * Fix calls to the parent constructor within a class.
  123. *
  124. * @param Tokens $tokens the Tokens instance
  125. * @param int $classStart the class start index
  126. * @param int $classEnd the class end index
  127. */
  128. private function fixParent(Tokens $tokens, $classStart, $classEnd)
  129. {
  130. // check calls to the parent constructor
  131. foreach ($tokens->findGivenKind(T_EXTENDS) as $index => $token) {
  132. $parentIndex = $tokens->getNextMeaningfulToken($index);
  133. $parentClass = $tokens[$parentIndex]->getContent();
  134. // using parent::ParentClassName()
  135. $parentSeq = $tokens->findSequence(array(
  136. array(T_STRING, 'parent'),
  137. array(T_DOUBLE_COLON),
  138. array(T_STRING, $parentClass),
  139. '(',
  140. ), $classStart, $classEnd, array(false, true, false, true));
  141. if (null !== $parentSeq) {
  142. // we only need indexes
  143. $parentSeq = array_keys($parentSeq);
  144. // replace method name with __construct
  145. $tokens[$parentSeq[2]]->setContent('__construct');
  146. }
  147. // using $this->ParentClassName()
  148. $parentSeq = $tokens->findSequence(array(
  149. array(T_VARIABLE, '$this'),
  150. array(T_OBJECT_OPERATOR),
  151. array(T_STRING, $parentClass),
  152. '(',
  153. ), $classStart, $classEnd, array(2 => false));
  154. if (null !== $parentSeq) {
  155. // we only need indexes
  156. $parentSeq = array_keys($parentSeq);
  157. // replace call with parent::__construct()
  158. $tokens[$parentSeq[0]] = new Token(array(
  159. T_STRING,
  160. 'parent',
  161. ));
  162. $tokens[$parentSeq[1]] = new Token(array(
  163. T_DOUBLE_COLON,
  164. '::',
  165. ));
  166. $tokens[$parentSeq[2]]->setContent('__construct');
  167. }
  168. }
  169. }
  170. /**
  171. * Generate the sequence of tokens necessary for the body of a wrapper method that simply
  172. * calls $this->{$method}( [args...] ) with the same arguments as its own signature.
  173. *
  174. * @param Tokens $tokens the Tokens instance
  175. * @param string $method the wrapped method name
  176. * @param int $startIndex function/method start index
  177. * @param int $bodyIndex function/method body index
  178. *
  179. * @return array an array containing the sequence and case sensitiveness [ 0 => $seq, 1 => $case ]
  180. */
  181. private function getWrapperMethodSequence(Tokens $tokens, $method, $startIndex, $bodyIndex)
  182. {
  183. // initialise sequence as { $this->{$method}(
  184. $seq = array(
  185. '{',
  186. array(T_VARIABLE, '$this'),
  187. array(T_OBJECT_OPERATOR),
  188. array(T_STRING, $method),
  189. '(',
  190. );
  191. $case = array(3 => false);
  192. // parse method parameters, if any
  193. $index = $startIndex;
  194. while (true) {
  195. // find the next variable name
  196. $index = $tokens->getNextTokenOfKind($index, array(array(T_VARIABLE)));
  197. if (null === $index || $index >= $bodyIndex) {
  198. // we've reached the body already
  199. break;
  200. }
  201. // append a comma if it's not the first variable
  202. if (count($seq) > 5) {
  203. $seq[] = ',';
  204. }
  205. // append variable name to the sequence
  206. $seq[] = array(T_VARIABLE, $tokens[$index]->getContent());
  207. }
  208. // almost done, close the sequence with ); }
  209. $seq[] = ')';
  210. $seq[] = ';';
  211. $seq[] = '}';
  212. return array($seq, $case);
  213. }
  214. /**
  215. * Find a function or method matching a given name within certain bounds.
  216. *
  217. * @param Tokens $tokens the Tokens instance
  218. * @param string $name the function/Method name
  219. * @param int $startIndex the search start index
  220. * @param int $endIndex the search end index
  221. *
  222. * @return array|null An associative array, if a match is found:
  223. *
  224. * - nameIndex (int): The index of the function/method name.
  225. * - startIndex (int): The index of the function/method start.
  226. * - endIndex (int): The index of the function/method end.
  227. * - bodyIndex (int): The index of the function/method body.
  228. * - modifiers (array): The modifiers as array keys and their index as
  229. * the values, e.g. array(T_PUBLIC => 10).
  230. */
  231. private function findFunction(Tokens $tokens, $name, $startIndex, $endIndex)
  232. {
  233. $function = $tokens->findSequence(array(
  234. array(T_FUNCTION),
  235. array(T_STRING, $name),
  236. '(',
  237. ), $startIndex, $endIndex, false);
  238. if (null === $function) {
  239. return;
  240. }
  241. // keep only the indexes
  242. $function = array_keys($function);
  243. // find previous block, saving method modifiers for later use
  244. $possibleModifiers = array(T_PUBLIC, T_PROTECTED, T_PRIVATE, T_STATIC, T_ABSTRACT, T_FINAL);
  245. $modifiers = array();
  246. $prevBlock = $tokens->getPrevMeaningfulToken($function[0]);
  247. while (null !== $prevBlock && $tokens[$prevBlock]->isGivenKind($possibleModifiers)) {
  248. $modifiers[$tokens[$prevBlock]->getId()] = $prevBlock;
  249. $prevBlock = $tokens->getPrevMeaningfulToken($prevBlock);
  250. }
  251. if (isset($modifiers[T_ABSTRACT])) {
  252. // abstract methods have no body
  253. $bodyStart = null;
  254. $funcEnd = $tokens->getNextTokenOfKind($function[2], array(';'));
  255. } else {
  256. // find method body start and the end of the function definition
  257. $bodyStart = $tokens->getNextTokenOfKind($function[2], array('{'));
  258. $funcEnd = $bodyStart !== null ? $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $bodyStart) : null;
  259. }
  260. return array(
  261. 'nameIndex' => $function[1],
  262. 'startIndex' => $prevBlock + 1,
  263. 'endIndex' => $funcEnd,
  264. 'bodyIndex' => $bodyStart,
  265. 'modifiers' => $modifiers,
  266. );
  267. }
  268. }