Видаляємо старий код автоматом

Недавно проводив чистку проекту і вирішив повидаляти частини коду які не будуть виконуватись. Так як я програміст я не збирався робити це все діло в ручну. В даній статті я покажу за допомогою яких інструментів я це все робив і як.

Давним давно

Дуже і дуже давно я мав справу з рефакторингом. Потрібно було замінити один застарілий компонент іншим. Тоді я і почав писати першу версію своєї тулзи. На даному етапі ця тулза переписана уже декілька раз і я створив окремий пакет для останньої версії, щоб не ламати старий код.

Алгоритм

Все дуже просто. Я постарався знайти код який іде після таких ключових слів як return, exit, die і грохнути його. Хтось скаже: це дуже просто зробити за допомогою рег! Я скажу: фіг там. Спробуй і у тебе нічого не вийде. В даному алгоритмі не буде описано таких штук як:

if (false) {
  echo 123;
}

і подібних. Можливо в майбутньому.

про інструмент

Використовував такі бібліотеки як:

"funivan/php-tokenizer": "dev-master",
"fabpot/php-cs-fixer": "^1.9",

Фіксер прикольний тим що можна без проблем додавати свої кастомні фіксери і бачити класні diff коду.

Бібліотека PhpTokenizer призначена для пошуку і маніпуляції словами (токенами) нашого коду.

Тобто ми беремо наш код, розбиваємо на токени (ця фішка вбудована у Рнр. Функція token_get_all ) пишемо певні умови, щоб визначити який код нам модифікувати, модифікуємо певні слова. Можливо дуже заморчено, але приклад поставить все на свої місця. Спочатку код був дуже простий, пізніше він розрісся аж у такого гіганта.

Добре що один раз написав, а потім юзай.Переваг цієї всієї штуки у порівнянні із ручним методом:

  1. Якщо у вас щось не вийшло, ви відкочуєте коміт а потім зможете запустити код ще раз.
  2. Можна обробити безліч коду.
  3. Доступний і в майбутньому.

P.S. Є така чудова штука як ast але нажаль у рнр версії до 5.6 вона не реалізована.

  use Funivan\PhpTokenizer\Collection;
  use Funivan\PhpTokenizer\Strategy\Search;
  use Funivan\PhpTokenizer\Strategy\Strict;
  use Funivan\PhpTokenizer\StreamProcess\StreamProcess;
  use Symfony\CS\AbstractFixer;
  use Symfony\CS\FixerInterface;

  /**
   *
   * @package Atl\Automation\Fixer
   */
  class RemoveUnreachableStatementFixer extends AbstractFixer {

    /**
     * @var callable
     */
    protected $customProcessStartDetector = null;

    /**
     * {@inheritdoc}
     */
    public function getLevel() {
      return FixerInterface::NONE_LEVEL;
    }


    /**
     * {@inheritdoc}
     */
    public function fix(\SplFileInfo $file, $content) {
      $collection = Collection::initFromString($content);

      $stream = new StreamProcess($collection, true);

      while ($p = $stream->getProcessor()) {

        # шукаємо ключові слова після яких у нас може знаходитись мертвий код
        $start = $p->process(Strict::create()->valueIs(['return', 'exit', 'die']));
        if ($p->isValid() == false and $this->customProcessStartDetector !== null) {
          # на даному етапі ця фішка ще не протестена 
          $p->setValid(true);
          $p->move(-1);
          $function = $this->customProcessStartDetector;
          $start = $function($p);
        }


        # пропускаємо такі штуки як return function(){ echo 1; }
        $lineEnd = $p->process(Search::create()->valueIs(['{', ';']));
        if ($lineEnd->getValue() == '{' or $lineEnd->getType() == T_ENCAPSED_AND_WHITESPACE) {
          # possible closure
          continue;
        }


        # від даної позиції ідемо шукати ключові слова які можуть бути мертвим кодом 
        $blockEnd = $p->process(Search::create()->valueIs(['}', 'break', 'default', 'case']));
        if (!$p->isValid()) {
          continue;
        }


        # ми маємо токени початку і токени після фрази return ;
        # якщо у цьому проміжку є тільки коментарі і пробіли відповідно цей код ми не рухаємо
        # а якщо є щось інше - значить це уже і є мертвий код 
        $hasDeadCode = false;
        $items = $collection->extractItems($lineEnd->getIndex() + 1, $blockEnd->getIndex() - $lineEnd->getIndex() - 1);
        foreach ($items as $token) {
          if (in_array($token->getType(), [T_WHITESPACE, T_COMMENT])) {
            continue;
          }
          $hasDeadCode = true;
          break;
        }

        if ($hasDeadCode == false) {
          continue;
        }


        $removeCode = $items;

        # на даному етапі нам відомо що у нас є мертвий код, але не знаємо де він закінчується
        # визначаємо область після return ; 
        if ($lineEnd->getValue() != '}') {
          $getTokenBlock = function ($startFromIndex) use ($p) {
            $p->moveTo($startFromIndex);
            $p->search('{', -1);
            $p->move(-2);
            $removeCode = $p->section('{', '}');
            return $removeCode;
          };

          $lastIndex = $lineEnd->getIndex();
          $blockEndIndex = $lineEnd->getIndex();
          $maxIterations = 20;
          do {

            /** @var Collection $removeCode */
            $removeCode = $getTokenBlock($lastIndex);

            if (count($removeCode) == 0) {
              break;
            }
            $lastIndexInBlock = $removeCode->getLast()->getIndex();

            if ($blockEndIndex > $lastIndexInBlock) {
              $lastIndex = $removeCode->getFirst()->getIndex() - 1;
            }

            $maxIterations--;
          } while (true and $maxIterations);

          foreach ($removeCode as $index => $tokenToRemove) {
            # ми визначили повністю область в якій знаходиться return ; 
            # в дану область попадають  токени до return і після return
            # видаляємо тільки ті які ідуть після  
            if ($tokenToRemove->getIndex() <= $lineEnd->getIndex()) {
              unset($removeCode[$index]);
            }
          }
          
          $removeCode->slice(0, -1);
        }

        # видаляємо старий код 
        $removeCode->remove();

      }

      return (string) $collection;
    }

    /**
     * {@inheritdoc}
     */
    public function getDescription() {
      return 'Replace dead code';
    }

    /**
     * @param callable $customProcessStartDetector
     * @return $this
     */
    public function setCustomProcessStartDetector(callable $customProcessStartDetector) {
      $this->customProcessStartDetector = $customProcessStartDetector;
      return $this;
    }

  }