Last active
January 28, 2025 15:46
-
-
Save oplanre/a8320becd9dce8c690dafbf8fedc46c7 to your computer and use it in GitHub Desktop.
PHP script to generate a single IDE stub file from php files in a project
This file contains hidden or 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
<?php | |
require_once __DIR__ . "/bootstrap.php"; | |
use PhpParser\Error; | |
use PhpParser\Node\Stmt\ClassMethod; | |
use PhpParser\Node\Stmt\Function_; | |
use PhpParser\Node\Stmt\Interface_; | |
use PhpParser\NodeTraverser; | |
use PhpParser\ParserFactory; | |
use PhpParser\PrettyPrinter; | |
use PhpParser\Node; | |
use PhpParser\NodeVisitorAbstract; | |
define('PARSER', (new ParserFactory)->createForHostVersion()); | |
define('PRINTER', new class extends PrettyPrinter\Standard { | |
private bool $inInterface = false; | |
private function printFunctionLike( | |
array $attrGroups, | |
Node\Identifier $name, | |
array $params, | |
?array $stmts, | |
string $functionType = '', | |
Node|null $returnType = null, | |
bool $abstract = false, | |
bool $byRef = false, | |
): string { | |
return $this->pAttrGroups($attrGroups) | |
. $functionType . 'function ' . ($byRef ? '&' : '') . $name | |
. '(' . $this->pMaybeMultiline($params, $this->phpVersion->supportsTrailingCommaInParamList()) . ')' | |
. (null !== $returnType ? ': ' . $this->p($returnType) : '') | |
. ( | |
($this->inInterface || $abstract || null === $stmts) | |
? ';' | |
: (count($stmts) === 0 ? ' {}' : ' {' . $this->nl . $this->pStmts($stmts) . $this->nl . '}' | |
) | |
); | |
} | |
protected function pStmt_ClassMethod(ClassMethod $node): string | |
{ | |
return $this->printFunctionLike( | |
attrGroups: $node->attrGroups, | |
functionType: $this->pModifiers($node->flags), | |
name: $node->name, | |
params: $node->params, | |
stmts: $node->stmts, | |
returnType: $node->returnType, | |
abstract: $node->isAbstract(), | |
byRef: $node->byRef, | |
); | |
} | |
protected function pStmt_Function(Function_ $node): string | |
{ | |
return $this->printFunctionLike( | |
attrGroups: $node->attrGroups, | |
name: $node->name, | |
params: $node->params, | |
stmts: $node->stmts, | |
returnType: $node->returnType, | |
); | |
} | |
protected function pStmt_Interface(Interface_ $node): string | |
{ | |
$this->inInterface = true; | |
$out = $this->pAttrGroups($node->attrGroups) | |
. 'interface ' . $node->name | |
. (!empty($node->extends) ? ' extends ' . $this->pCommaSeparated($node->extends) : '') | |
. $this->nl . '{' . $this->pStmts($node->stmts) . $this->nl . '}'; | |
$this->inInterface = false; | |
return $out; | |
} | |
}); | |
define('VISITOR', new class extends NodeVisitorAbstract { | |
public function leaveNode(Node $node) | |
{ | |
if ($node instanceof Node\Stmt\Function_ || ($node instanceof Node\Stmt\ClassMethod && !$node->isPrivate())) { | |
$node->stmts = []; | |
return $node; | |
} | |
if ( | |
( | |
$node instanceof Node\Stmt\Use_ || | |
$node instanceof Node\Stmt\GroupUse || | |
$node instanceof Node\Stmt\Declare_ | |
) || | |
(( | |
$node instanceof Node\Stmt\ClassConst || | |
$node instanceof Node\Stmt\ClassMethod || | |
$node instanceof Node\Stmt\Property | |
) && $node->isPrivate()) | |
) { | |
return NodeTraverser::REMOVE_NODE; | |
} | |
if ($node instanceof Node\Stmt\PropertyProperty) { | |
$node->default = null; | |
return $node; | |
} | |
if ($node instanceof Node\Stmt\ClassLike) { | |
return new Node\Stmt\If_( | |
cond: new Node\Expr\BooleanNot( | |
new Node\Expr\FuncCall( | |
new Node\Name(match (true) { | |
$node instanceof Node\Stmt\Enum_ => 'enum_exists', | |
$node instanceof Node\Stmt\Interface_ => 'interface_exists', | |
$node instanceof Node\Stmt\Trait_ => 'trait_exists', | |
default => 'class_exists', | |
}), | |
[ | |
new Node\Arg( | |
new Node\Scalar\String_( | |
$node->namespacedName?->toString() ?? $node->name?->name ?? '' | |
), | |
), | |
], | |
), | |
), | |
subNodes: ['stmts' => [$node]], | |
); | |
} | |
if (!$node instanceof Node\Stmt\Namespace_) { | |
$node->setAttribute('comments', []); | |
} | |
return $node; | |
} | |
}); | |
class Stubbifier | |
{ | |
private array $namespaces = []; | |
public function __construct() {} | |
public function walkNodes(array $nodes) | |
{ | |
foreach ($nodes as $node) { | |
if ($node instanceof Node\Stmt\Namespace_) { | |
// If we find a namespace, process its statements recursively | |
$this->processNamespace($node); | |
} else { | |
// Handle non-namespace nodes (could go into "global" namespace) | |
$this->ns(null, [$node]); | |
} | |
} | |
} | |
private function processNamespace(Node\Stmt\Namespace_ $namespace) | |
{ | |
$stmts = []; | |
$nestedNamespaces = []; | |
// Separate nested namespaces from other statements | |
foreach ($namespace->stmts as $stmt) { | |
if ($stmt instanceof Node\Stmt\Namespace_) { | |
$nestedNamespaces[] = $stmt; | |
} else { | |
$stmts[] = $stmt; | |
} | |
} | |
// Handle the current namespace's direct statements | |
if ($stmts) { | |
$this->ns($namespace->name, $stmts, $namespace->getAttributes()); | |
} | |
// Process any nested namespaces | |
foreach ($nestedNamespaces as $nested) { | |
if ($namespace->name !== null) { | |
// Combine the namespace names | |
$newName = Node\Name::concat($namespace->name, $nested->name); | |
$nested->name = $newName; | |
} | |
$this->processNamespace($nested); | |
} | |
} | |
public function ns(?Node\Name $name, array $stmts, array $attributes = []) | |
{ | |
$key = $name?->toString() ?? ''; | |
$ns = $this->namespaces[$key] ?? new Node\Stmt\Namespace_( | |
name: $name, | |
attributes: array_merge($attributes, [ | |
'kind' => Node\Stmt\Namespace_::KIND_BRACED, | |
]), | |
); | |
$ns->stmts = array_merge($ns->stmts ?? [], $stmts); | |
$this->namespaces[$key] = $ns; | |
} | |
public function __invoke(\Traversable $it, string $dest = STUB_DIR . "/ptst.stub.php") | |
{ | |
try { | |
foreach ($it as $file) { | |
$sourceCode = file_get_contents($file->getRealPath()); | |
$traverser = new NodeTraverser; | |
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver); | |
$traverser->addVisitor(VISITOR); | |
$this->walkNodes( | |
$traverser->traverse( | |
PARSER->parse($sourceCode), | |
), | |
); | |
} | |
@mkdir(dirname($dest), 0777, true); | |
file_put_contents( | |
$dest, | |
PRINTER->prettyPrintFile(array_values($this->namespaces)), | |
); | |
} catch (Error $error) { | |
throw new RuntimeException("Parse error: {$error->getMessage()}"); | |
} | |
} | |
} | |
exec("rm -rf " . STUB_DIR); | |
(new Stubbifier)((new Symfony\Component\Finder\Finder)->files()->in(PHP_DIR . "/src")->name("*.php")); | |
echo "Stubs generated\n"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
good