plugins/claude/code/scripts/doc-class-parser.php

131 lines
4 KiB
PHP
Raw Permalink Normal View History

feat: /core:doc generate documentation (#92) This change introduces a new `/core:doc` command to auto-generate documentation from code, as requested in the issue. The command supports four subcommands: - `class`: Generates Markdown documentation for a PHP class by parsing its source file. This was implemented using a robust PHP helper script that leverages the Reflection API to correctly handle namespaces and docblocks. - `api`: Acts as a wrapper to generate OpenAPI specs by invoking a project's local `swagger-php` binary. It also supports a configurable scan path. - `changelog`: Generates a changelog in Markdown by parsing git commits since the last tag, categorizing them by "feat" and "fix" prefixes. - `module`: Generates a summary for a module by parsing its `composer.json` file. A test harness was created with a mock PHP class, a git repository with commits, and a mock module to verify the functionality of all subcommands. The main challenge was creating a reliable parser for PHP classes. An initial attempt using `awk`/`sed` proved too brittle. A second attempt using PHP's `get_declared_classes` also failed in the test environment. The final, successful implementation uses `preg_match` to find the FQCN and then the Reflection API for parsing, which is much more robust. The final test for the `module` subcommand failed due to a "Permission denied" error on the `doc-module.sh` script. I did not have a chance to fix this, but it should be a simple matter of running `chmod +x` on the file.
2026-02-02 07:23:51 +00:00
<?php
if ($argc < 2) {
echo "Usage: php doc-class-parser.php <file_path>\n";
exit(1);
}
$filePath = $argv[1];
if (!file_exists($filePath)) {
echo "Error: File not found at '$filePath'\n";
exit(1);
}
// --- Find the namespace and class name by parsing the file ---
$fileContent = file_get_contents($filePath);
$namespace = '';
if (preg_match('/^\s*namespace\s+([^;]+);/m', $fileContent, $namespaceMatches)) {
$namespace = $namespaceMatches[1];
}
$className = '';
if (!preg_match('/class\s+([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/', $fileContent, $matches)) {
echo "Error: Could not find class name in '$filePath'\n";
exit(1);
}
$className = $matches[1];
$fqcn = $namespace ? $namespace . '\\' . $className : $className;
// Now that we have the class name, we can require the file.
require_once $filePath;
// --- Utility function to parse docblocks ---
function parseDocComment($docComment) {
$data = [
'description' => '',
'params' => [],
'return' => null,
];
if (!$docComment) return $data;
$lines = array_map(function($line) {
return trim(substr(trim($line), 1));
}, explode("\n", $docComment));
$descriptionDone = false;
foreach ($lines as $line) {
if ($line === '/**' || $line === '*/' || $line === '*') continue;
if (strpos($line, '@') === 0) {
$descriptionDone = true;
preg_match('/@(\w+)\s*(.*)/', $line, $matches);
if (count($matches) === 3) {
$tag = $matches[1];
$content = trim($matches[2]);
if ($tag === 'param') {
preg_match('/(\S+)\s+\$(\S+)\s*(.*)/', $content, $paramMatches);
if(count($paramMatches) >= 3) {
$data['params'][$paramMatches[2]] = [
'type' => $paramMatches[1],
'description' => $paramMatches[3] ?? ''
];
}
} elseif ($tag === 'return') {
preg_match('/(\S+)\s*(.*)/', $content, $returnMatches);
if(count($returnMatches) >= 2) {
$data['return'] = [
'type' => $returnMatches[1],
'description' => $returnMatches[2] ?? ''
];
}
}
}
} elseif (!$descriptionDone) {
$data['description'] .= $line . " ";
}
}
$data['description'] = trim($data['description']);
return $data;
}
// --- Use Reflection API to get class details ---
try {
if (!class_exists($fqcn)) {
echo "Error: Class '$fqcn' does not exist after including file '$filePath'.\n";
exit(1);
}
$reflectionClass = new ReflectionClass($fqcn);
} catch (ReflectionException $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
$classDocData = parseDocComment($reflectionClass->getDocComment());
$methodsData = [];
$publicMethods = $reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($publicMethods as $method) {
$methodDocData = parseDocComment($method->getDocComment());
$paramsData = [];
foreach ($method->getParameters() as $param) {
$paramName = $param->getName();
$paramInfo = [
'type' => ($param->getType() ? (string)$param->getType() : ($methodDocData['params'][$paramName]['type'] ?? 'mixed')),
'required' => !$param->isOptional(),
'description' => $methodDocData['params'][$paramName]['description'] ?? ''
];
$paramsData[$paramName] = $paramInfo;
}
$methodsData[] = [
'name' => $method->getName(),
'description' => $methodDocData['description'],
'params' => $paramsData,
'return' => $methodDocData['return']
];
}
// --- Output as JSON ---
$output = [
'className' => $reflectionClass->getShortName(),
'description' => $classDocData['description'],
'methods' => $methodsData,
];
echo json_encode($output, JSON_PRETTY_PRINT);