Skip to content

Instantly share code, notes, and snippets.

@pounard
Last active July 31, 2024 10:59
Show Gist options
  • Save pounard/7ad24b39c5e51a76105f46c1e06e3ea6 to your computer and use it in GitHub Desktop.
Save pounard/7ad24b39c5e51a76105f46c1e06e3ea6 to your computer and use it in GitHub Desktop.
Finds duplicated functions names in PHP codebase
<?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";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment