Skip to content

Instantly share code, notes, and snippets.

@grom358
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grom358/9106837 to your computer and use it in GitHub Desktop.
Save grom358/9106837 to your computer and use it in GitHub Desktop.
#!/usr/bin/php
<?php
/**
* PHP Token
*/
class Token {
public $type;
public $text;
public $lineNo;
public $colNo;
public function __construct($type, $text, $lineNo = NULL, $colNo = NULL) {
$this->type = $type;
$this->text = $text;
$this->lineNo = $lineNo;
$this->colNo = $colNo;
}
static public function typeName($type) {
if (is_string($type)) {
return $type;
} else {
return token_name($type);
}
}
public function getTypeName() {
return self::typeName($this->type);
}
public function __toString() {
return $this->lineNo . ':' . $this->colNo . ' => ' . $this->getTypeName() . '|' . $this->text;
}
}
/**
* Convert PHP source file into array of tokens
*/
class Tokenizer {
private $lineNo;
private $colNo;
private function parseToken($token) {
if (is_array($token)) {
$type = $token[0];
$text = $token[1];
} else {
$type = $token;
$text = $token;
}
$lineNo = $this->lineNo;
$colNo = $this->colNo;
$line_count = substr_count($text, "\n");
if ($line_count > 0) {
$this->lineNo += $line_count;
$lines = split("\n", $text);
$last_line = end($lines);
$this->colNo = strlen($last_line);
if ($this->colNo === 0) {
$this->colNo = 1;
}
} else {
$this->colNo += strlen($text);
}
return new Token($type, $text, $lineNo, $colNo);
}
public function getAll($source) {
$this->colNo = 1;
$this->lineNo = 1;
$tokens = array();
foreach (token_get_all($source) as $rawToken) {
$tokens[] = $this->parseToken($rawToken);
}
return $tokens;
}
}
/**
* Find PHP files in this directory and sub directorys and invoke a callback passing the filename
*
* @param $callback the callback function to call with filename
*/
function find_drupal_php_files($callback) {
$directory = new RecursiveDirectoryIterator('.');
$iterator = new RecursiveIteratorIterator($directory);
$regex = new RegexIterator($iterator, '/^.+\.(php|inc|module|install)$/i', RecursiveRegexIterator::GET_MATCH);
foreach ($regex as $name => $object) {
$callback($name);
}
}
/**
* Test if $str starts with the string $test
*
* @param string $str The subject string
* @param string $test The matching string
* @return bool True if the subject string starts with the matching string
*/
function str_starts_with($str, $test) {
return substr($str, 0, strlen($test)) === $test;
}
/**
* Consume a stream of tokens
*/
class TokenConsumer {
private $position;
private $length;
private $tokens;
public function __construct($tokens) {
$this->position = 0;
$this->length = count($tokens);
$this->tokens = $tokens;
}
public function getPosition() {
return $this->position;
}
public function getPrevious() {
$previous = NULL;
$prevPosition = $this->position;
do {
$prevPosition = $prevPosition - 1;
if ($prevPosition < 0) {
return NULL;
}
$prevToken = $this->tokens[$prevPosition];
} while ($prevToken->type === T_WHITESPACE || $prevToken->type === T_COMMENT || $prevToken->type === T_DOC_COMMENT);
return $prevToken;
}
public function getCurrent($skipHidden = TRUE) {
if ($this->position >= $this->length) {
return NULL;
}
$token = $this->tokens[$this->position];
while ($skipHidden && ($token->type === T_WHITESPACE || $token->type === T_COMMENT || $token->type === T_DOC_COMMENT)) {
$this->position++;
if ($this->position >= $this->length) {
return NULL;
}
$token = $this->tokens[$this->position];
}
return $token;
}
public function skipWhitespace() {
if ($this->position >= $this->length) {
return;
}
while (TRUE) {
$token = $this->tokens[$this->position];
if ($token->type === T_WHITESPACE) {
$this->position++;
} else {
break;
}
}
}
public function isCurrent($type) {
return $this->getCurrent()->type === $type;
}
private function _getNext() {
$nextToken = NULL;
$nextPosition = $this->position;
do {
$nextPosition = $nextPosition + 1;
if ($nextPosition >= $this->length) {
return NULL;
}
$nextToken = $this->tokens[$nextPosition];
} while ($nextToken->type === T_WHITESPACE || $nextToken->type === T_COMMENT || $nextToken->type === T_DOC_COMMENT);
return array(
'token' => $nextToken,
'position' => $nextPosition,
);
}
public function getNext() {
$next = $this->_getNext();
if ($next === NULL) {
return NULL;
}
return $next['token'];
}
public function isNext($type) {
$nextToken = $this->getNext();
if ($nextToken !== NULL) {
return $nextToken->type === $type;
}
return FALSE;
}
public function consume($type = NULL, $skipHidden = TRUE) {
if ($type === NULL || $this->isCurrent($type)) {
$token = $this->getCurrent($skipHidden);
$this->position++;
return $token;
} else {
$current = $this->getCurrent($skipHidden);
$currentType = $current ? $current->getTypeName() : NULL;
throw new Exception('Mismatch expected type:' . Token::typeName($type) . ' but got:' . $currentType);
}
}
}
function declaration_sort($a, $b) {
$a_path = ltrim($a['declaration'], '\\');
$b_path = ltrim($b['declaration'], '\\');
return strnatcmp($a_path, $b_path);
}
class FunctionCallReplacer {
private $useDeclaration;
private $alias;
private $oldFunctionName;
private $methodName;
private $mode;
private $buffer;
public function __construct($useDeclaration, $alias, $oldFunctionName, $methodName) {
$this->useDeclaration = $useDeclaration;
$parts = explode('\\', $useDeclaration);
$this->className = $parts[count($parts) - 1];
$this->alias = $alias;
$this->oldFunctionName = $oldFunctionName;
$this->methodName = $methodName;
}
public function process($tokens) {
$stream = new TokenConsumer($tokens);
$this->mode = 'global';
$namespaces = array();
$namespaces[] = array(
'namespace' => '_top',
'use_declarations' => array(),
'use_classes' => array(),
'function_call_positions' => array(),
);
if ($stream->isCurrent(T_OPEN_TAG)) {
$stream->consume(T_OPEN_TAG);
$stream->skipWhitespace();
$comment = $stream->getCurrent(FALSE);
if ($comment->type === T_DOC_COMMENT) {
if (str_starts_with($comment->text, "/**\n * @file")) {
$namespaces[count($namespaces) - 1]['file_comment_position'] = $stream->getPosition();
}
}
}
while ($token = $stream->getCurrent()) {
$callback = array($this, 'process' . ucfirst($this->mode));
$callback($stream, $namespaces);
}
foreach ($namespaces as $namespace) {
if (!empty($namespace['function_call_positions'])) {
// Generate use declaration block
$names = array();
$alreadyImported = FALSE;
foreach ($namespace['use_declarations'] as $use_declaration) {
$names[] = $use_declaration['class_name'];
if ($use_declaration['declaration'] === $this->useDeclaration) {
$alreadyImported = TRUE;
}
}
if (!$alreadyImported) {
$alias = NULL;
if (in_array($this->className, $names)) {
$alias = $this->alias;
}
$namespace['use_declarations'][] = array(
'declaration' => $this->useDeclaration,
'alias' => $alias,
);
}
usort($namespace['use_declarations'], 'declaration_sort');
$code = '';
foreach ($namespace['use_declarations'] as $i => $use_declaration) {
if ($i > 0) {
$code .= "\n";
}
$code .= 'use ' . $use_declaration['declaration'];
$parts = explode('\\', $use_declaration['declaration']);
$class_name = $parts[count($parts) - 1];
if ($use_declaration['alias']) {
$code .= ' as ' . $use_declaration['alias'];
}
$code .= ';';
}
$use_block = new Token(T_USE, $code);
// Replace function calls
$funcName = $this->className . '::' . $this->methodName;
if ($alias) {
$funcName = $alias . '::' . $this->methodName;
}
foreach ($namespace['function_call_positions'] as $position) {
$tokens[$position] = new Token(T_STRING, $funcName);
}
// Insert use declaration
if (isset($namespace['use_declarations_end_position'])) {
$len = $namespace['use_declarations_end_position'] - $namespace['use_declarations_start_position'];
$ws = new Token(T_WHITESPACE, '');
$replacement = array_fill(0, $len, $ws);
$replacement[0] = $use_block;
array_splice($tokens, $namespace['use_declarations_start_position'], $len, $replacement);
}
elseif (isset($namespace['namespace_end_position'])) {
$ws = new Token(T_WHITESPACE, "\n\n");
array_splice($tokens, $namespace['namespace_end_position'] + 1, 0, array($ws, $use_block));
}
elseif (isset($namespace['file_comment_position'])) {
$ws = new Token(T_WHITESPACE, "\n\n");
array_splice($tokens, $namespace['file_comment_position'] + 1, 0, array($ws, $use_block));
}
else {
throw new Exception('unhandled insert');
}
// Write back tokens
$source = '';
foreach ($tokens as $token) {
$source .= $token->text;
}
return $source;
}
}
return NULL;
}
private function processGlobal($stream, &$namespaces) {
$token = $stream->getCurrent();
if ($token->type === T_NAMESPACE) {
$stream->consume(T_NAMESPACE);
$this->buffer = '';
$this->mode = 'namespace';
}
elseif ($token->type === T_USE && ($stream->isNext(T_STRING) || $stream->isNext(T_NS_SEPARATOR))) {
$namespace =& $namespaces[count($namespaces) - 1];
if (isset($namespace['use_declarations_start_position'])) {
throw new Exception('invalid use declaration:' . $token);
}
$namespace['use_declarations_start_position'] = $stream->getPosition();
$this->mode = 'useDeclarationBlock';
}
elseif ($token->type === T_STRING && $token->text === $this->oldFunctionName &&
$stream->isNext('(') && $stream->getPrevious()->type !== T_FUNCTION) {
$namespace =& $namespaces[count($namespaces) - 1];
$namespace['function_call_positions'][] = $stream->getPosition();
$stream->consume();
}
else {
$stream->consume();
}
}
private function processUseDeclarationBlock($stream, &$namespaces) {
$token = $stream->getCurrent();
if ($token->type === T_USE && ($stream->isNext(T_STRING) || $stream->isNext(T_NS_SEPARATOR))) {
$stream->consume(T_USE);
$this->buffer = '';
$this->mode = 'useDeclaration';
}
else {
$this->mode = 'global';
}
}
private function processUseDeclaration($stream, &$namespaces) {
static $alias = NULL;
$token = $stream->getCurrent();
if ($token->type === ';') {
$class_name = $stream->getPrevious()->text;
$stream->consume(';');
$namespace =& $namespaces[count($namespaces) - 1];
$namespace['use_declarations_end_position'] = $stream->getPosition();
$namespace['use_declarations'][] = array(
'declaration' => $this->buffer,
'class_name' => $class_name,
'alias' => $alias,
);
$this->mode = 'useDeclarationBlock';
}
elseif ($token->type === ',') {
$stream->consume();
}
elseif ($token->type === T_STRING || $token->type === T_NS_SEPARATOR) {
$alias = NULL;
$this->buffer .= $token->text;
$stream->consume();
}
elseif ($token->type === T_AS && $stream->isNext(T_STRING)) {
$stream->consume(T_AS);
$alias = $stream->consume(T_STRING)->text;
}
else {
throw new Exception('Syntax error:' . $token);
}
}
private function processNamespace($stream, &$namespaces) {
$token = $stream->getCurrent();
if ($token->type === '{' || $token->type === ';') {
$namespaces[] = array(
'namespace' => $this->buffer,
'namespace_end_position' => $stream->getPosition(),
'use_declarations' => array(),
'use_classes' => array(),
'function_call_positions' => array(),
);
$this->mode = 'global';
}
elseif ($token->type === T_STRING || $token->type === T_NS_SEPARATOR) {
$this->buffer .= $token->text;
$stream->consume();
}
else {
throw new Exception('syntax error:' . $token);
}
}
}
$useDeclaration = $argv[1];
$alias = $argv[2];
$oldFunctionName = $argv[3];
$methodName = $argv[4];
$replacer = new FunctionCallReplacer($useDeclaration, $alias, $oldFunctionName, $methodName);
$tokenizer = new Tokenizer();
function code_upgrade($filename) {
global $replacer, $tokenizer;
$source = file_get_contents($filename);
$tokens = $tokenizer->getAll($source);
try {
$modified_source = $replacer->process($tokens);
if ($modified_source === NULL) {
return;
}
file_put_contents($filename, $modified_source);
} catch (Exception $e) {
echo 'Unable to process:' . $filename . PHP_EOL;
echo $e->getMessage() . PHP_EOL;
}
}
find_drupal_php_files('code_upgrade');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment