<?php /** * Run in folder where to scan: * php -f /path/to/duplicated_functions.php * * For more detailed debug information: * DEBUG=1 php -f /path/to/duplicated_functions.php */ declare (strict_types=1); function display(string $message, ?string $prefix = null) { if ($prefix) { echo "[" . $prefix . "] "; } echo $message . "\n"; } function debug(string $message, ?string $prefix = null) { if (getenv('DEBUG')) { display($message, $prefix); } } function warning(string $message, ?string $prefix = null) { display($message, $prefix ?? 'WARNING'); } function listfilesin(string $dirname, string $pattern = '@\.(php|inc|module|install)@'): array { $handle = opendir($dirname); $ret = []; while ($filename = readdir($handle)) { if ('.' === $filename || '..' === $filename) { continue; } $absolute = $dirname . '/' . $filename; if (is_dir($absolute)) { foreach (listfilesin($absolute) as $nested) { $ret[] = $nested; } } else if (preg_match($pattern, $filename)) { $ret[] = $absolute; } else { debug($absolute, 'ignoring'); } } return $ret; } class FunctionOccurence { public function __construct( public string $name, public string $filename, public int $line, ) {} } class FunctionOccurenceSet { public int $count = 0; /** @var FunctionOccurence[] */ public array $occurences = []; public function __construct( public string $name ) {} public function add(string $filename, int $line) { $this->occurences[] = new FunctionOccurence($this->name, $filename, $line); $this->count++; } } class FunctionMap { public array $functions = []; public function register(string $name, string $filename, int $line) { $set = ($this->functions[$name] ??= new FunctionOccurenceSet($name)); assert($set instanceof FunctionOccurenceSet); $set->add($filename, $line); } /** @return FunctionOccurenceSet[] */ public function findDuplicates(): iterable { foreach ($this->functions as $set) { if (1 < $set->count) { yield $set; } } } } $map = new FunctionMap(); foreach (listfilesin(getcwd()) as $filename) { debug($filename, 'found'); $data = file_get_contents($filename); if ($data) { $depth = 0; $inFunctionDeclaration = false; $inStringOrComplexExpr = false; foreach (PhpToken::tokenize($data) as $token) { assert($token instanceof PhpToken); if ($token->isIgnorable()) { continue; } // Ignore "${EXPR}" if ($token->is(T_DOLLAR_OPEN_CURLY_BRACES) || $token->is(T_CURLY_OPEN)) { $inStringOrComplexExpr = true; continue; } $name = $token->getTokenName(); if ('{' === $name) { $depth++; } else if ('}' === $name) { if ($inStringOrComplexExpr) { $inStringOrComplexExpr = false; } else { $depth--; } } if (0 > $depth) { warning(sprintf("Missed at least one opening brace '{' in '%s' at line %d", $filename, $token->line)); $depth = 0; // Avoid repeating the warning. } if (0 < $depth) { // Looking for root namespace functions, ignoring depths. continue; } if ($token->is(T_FUNCTION)) { $inFunctionDeclaration = true; } else if ($token->is(T_WHITESPACE)) { // Ignore that. } else if ($token->is(T_STRING)) { if ($inFunctionDeclaration) { // Function name. $map->register($token->text, $filename, $token->line); $inFunctionDeclaration = false; } } } } else { warning(sprintf("Could not read file '%s'", $filename)); } } foreach ($map->findDuplicates() as $set) { assert($set instanceof FunctionOccurenceSet); echo $set->name . "()\n"; foreach ($set->occurences as $occurence) { assert($occurence instanceof FunctionOccurence); echo " - " . $occurence->filename . " at line " . $occurence->line . "\n"; } }