Last active
August 29, 2015 13:56
-
-
Save grom358/9106837 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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