Skip to content

Commit 494b2c9

Browse files
committed
Add support for generating classynopses from stubs
1 parent ff8e04a commit 494b2c9

File tree

1 file changed

+250
-10
lines changed

1 file changed

+250
-10
lines changed

build/gen_stub.php

Lines changed: 250 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,13 +1015,16 @@ class PropertyInfo
10151015
public $type;
10161016
/** @var Expr|null */
10171017
public $defaultValue;
1018+
/** @var bool */
1019+
public $isReadonly;
10181020

1019-
public function __construct(PropertyName $name, int $flags, ?Type $type, ?Expr $defaultValue)
1021+
public function __construct(PropertyName $name, int $flags, ?Type $type, ?Expr $defaultValue, bool $isReadonly)
10201022
{
10211023
$this->name = $name;
10221024
$this->flags = $flags;
10231025
$this->type = $type;
10241026
$this->defaultValue = $defaultValue;
1027+
$this->isReadonly = $isReadonly;
10251028
}
10261029

10271030
public function discardInfoForOldPhpVersions(): void {
@@ -1184,6 +1187,42 @@ private function getFlagsAsString(): string
11841187

11851188
return $flags;
11861189
}
1190+
1191+
public function getFieldSynopsisElement(DOMDocument $doc): DOMElement
1192+
{
1193+
$fieldsynopsisElement = $doc->createElement("fieldsynopsis");
1194+
1195+
if ($this->flags & Class_::MODIFIER_PUBLIC) {
1196+
$fieldsynopsisElement->appendChild($doc->createElement("modifier", "public"));
1197+
} elseif ($this->flags & Class_::MODIFIER_PROTECTED) {
1198+
$fieldsynopsisElement->appendChild($doc->createElement("modifier", "protected"));
1199+
} elseif ($this->flags & Class_::MODIFIER_PRIVATE) {
1200+
$fieldsynopsisElement->appendChild($doc->createElement("modifier", "private"));
1201+
}
1202+
1203+
if ($this->flags & Class_::MODIFIER_STATIC) {
1204+
$fieldsynopsisElement->appendChild($doc->createElement("modifier", "static"));
1205+
} elseif ($this->flags & Class_::MODIFIER_READONLY || $this->isReadonly) {
1206+
$fieldsynopsisElement->appendChild($doc->createElement("modifier", "readonly"));
1207+
}
1208+
1209+
if ($this->type) {
1210+
$typeElement = $doc->createElement("type");
1211+
$fieldsynopsisElement->appendChild($typeElement);
1212+
}
1213+
1214+
$className = str_replace("\\", "-", $this->name->class->toLowerString());
1215+
$varnameElement = $doc->createElement("varname", $this->name->property);
1216+
$varnameElement->setAttribute("linkend", "$className.props." . $this->name->property);
1217+
$fieldsynopsisElement->appendChild($varnameElement);
1218+
1219+
if ($this->defaultValue) {
1220+
$initializerElement = $doc->createElement("initializer", "");
1221+
$fieldsynopsisElement->appendChild($initializerElement);
1222+
}
1223+
1224+
return $fieldsynopsisElement;
1225+
}
11871226
}
11881227

11891228
class ClassInfo {
@@ -1332,6 +1371,151 @@ private function getFlagsAsString(): string
13321371

13331372
return implode("|", $flags);
13341373
}
1374+
1375+
public function getClassSynopsisDocument(): ?string {
1376+
1377+
$doc = new DOMDocument();
1378+
$doc->formatOutput = true;
1379+
$classSynopsis = $this->getClassSynopsisElement($doc);
1380+
if (!$classSynopsis) {
1381+
return null;
1382+
}
1383+
1384+
$doc->appendChild($classSynopsis);
1385+
1386+
return $doc->saveXML();
1387+
}
1388+
1389+
public function getClassSynopsisElement(DOMDocument $doc): ?DOMElement {
1390+
1391+
$classSynopsis = $doc->createElement("classsynopsis");
1392+
$classSynopsis->appendChild(new DOMText("\n "));
1393+
1394+
$ooElement = self::createOoElement($doc, $this, false, false);
1395+
$classSynopsis->appendChild($ooElement);
1396+
1397+
$classSynopsis->appendChild(new DOMText("\n "));
1398+
1399+
$classSynopsisInfo = $doc->createElement("classsynopsisinfo");
1400+
$classSynopsisInfo->appendChild(new DOMText("\n "));
1401+
$ooElement = self::createOoElement($doc, $this, true, false);
1402+
$classSynopsisInfo->appendChild($ooElement);
1403+
$classSynopsisInfo->appendChild(new DOMText("\n "));
1404+
1405+
$classSynopsis->appendChild($classSynopsisInfo);
1406+
$classSynopsis->appendChild(new DOMText("\n "));
1407+
1408+
foreach ($this->extends as $k => $parent) {
1409+
$ooElement = self::createOoElement($doc, $this, false, $k === 0); // TODO fix isExtends for interfaces
1410+
$classSynopsisInfo->appendChild($ooElement);
1411+
$classSynopsisInfo->appendChild(new DOMText("\n "));
1412+
}
1413+
1414+
if ($this->implements) {
1415+
$ooElement = self::createOoElement($doc, $this, false, false);
1416+
$classSynopsisInfo->appendChild($ooElement);
1417+
$classSynopsisInfo->appendChild(new DOMText("\n "));
1418+
}
1419+
1420+
if (!empty($this->propertyInfos)) {
1421+
/*
1422+
<fieldsynopsis>
1423+
<modifier>public</modifier>
1424+
<modifier>readonly</modifier>
1425+
<type>string</type>
1426+
<varname linkend="domdocument.props.actualencoding">actualEncoding</varname>
1427+
</fieldsynopsis>
1428+
*/
1429+
1430+
$classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Properties;");
1431+
$classSynopsisInfo->setAttribute("role", "comment");
1432+
$classSynopsis->appendChild($classSynopsisInfo);
1433+
$classSynopsis->appendChild(new DOMText("\n "));
1434+
1435+
foreach ($this->propertyInfos as $propertyInfo) {
1436+
$fieldSynopsisElement = $propertyInfo->getFieldSynopsisElement($doc);
1437+
$classSynopsis->appendChild($fieldSynopsisElement);
1438+
$classSynopsisInfo->appendChild(new DOMText("\n "));
1439+
}
1440+
}
1441+
1442+
if (!empty($this->funcInfos)) {
1443+
$classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Methods;");
1444+
$classSynopsisInfo->setAttribute("role", "comment");
1445+
$classSynopsis->appendChild($classSynopsisInfo);
1446+
$classSynopsis->appendChild(new DOMText("\n "));
1447+
1448+
$className = $this->getClassSynopsisFilename();
1449+
1450+
$includeElement = $doc->createElement("xi:include");
1451+
$includeElement->setAttribute("xpointer", "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('class.$className')/db:refentry/db:refsect1[@role='description']/descendant::db:constructorsynopsis[not(@role='procedural')]");
1452+
$includeElement->appendChild(new DOMText("\n "));
1453+
$fallbackElement = $doc->createElement("xi:fallback");
1454+
$includeElement->appendChild($fallbackElement);
1455+
$includeElement->appendChild(new DOMText("\n "));
1456+
$classSynopsis->appendChild($includeElement);
1457+
$classSynopsis->appendChild(new DOMText("\n "));
1458+
1459+
$includeElement = $doc->createElement("xi:include");
1460+
$includeElement->setAttribute("xpointer", "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('class.$className')/db:refentry/db:refsect1[@role='description']/descendant::db:methodsynopsis[1])");
1461+
$classSynopsis->appendChild($includeElement);
1462+
$classSynopsis->appendChild(new DOMText("\n "));
1463+
}
1464+
1465+
return $classSynopsis;
1466+
}
1467+
1468+
private function createOoElementWithModifiers(DOMDocument $doc): DOMElement {
1469+
return self::createOoElement($doc, $this, true, false);
1470+
}
1471+
1472+
private static function createOoElement(DOMDocument $doc, ClassInfo $classInfo, bool $withModifiers, bool $isExtends): DOMElement {
1473+
if ($classInfo->type !== "class" && $classInfo->type !== "interface") {
1474+
throw new Exception("Class synopsis generation is not implemented for " . $classInfo->type);
1475+
}
1476+
1477+
$ooElement = $doc->createElement('oo' . $classInfo->type);
1478+
if ($isExtends) {
1479+
$ooElement = $doc->createElement('modifier', 'extends');
1480+
$ooElement->appendChild(new DOMText("\n "));
1481+
} elseif ($withModifiers) {
1482+
if ($classInfo->flags & Class_::MODIFIER_FINAL) {
1483+
$ooElement->appendChild($doc->createElement('modifier', 'final'));
1484+
$ooElement->appendChild(new DOMText("\n "));
1485+
}
1486+
if ($classInfo->flags & Class_::MODIFIER_ABSTRACT) {
1487+
$ooElement->appendChild($doc->createElement('modifier', 'abstract'));
1488+
$ooElement->appendChild(new DOMText("\n "));
1489+
}
1490+
}
1491+
1492+
$ooElement->appendChild(new DOMText("\n "));
1493+
$nameElement = $doc->createElement($classInfo->type . 'name', $classInfo->name->toString());
1494+
$ooElement->appendChild($nameElement);
1495+
$ooElement->appendChild(new DOMText("\n "));
1496+
1497+
return $ooElement;
1498+
}
1499+
1500+
public function getClassSynopsisFilename(): string {
1501+
return strtolower(implode('-', $this->name->parts));
1502+
}
1503+
1504+
private function appendMethodSynopsisTypeToElement(DOMDocument $doc, DOMElement $elementToAppend, Type $type) {
1505+
if (count($type->types) > 1) {
1506+
$typeElement = $doc->createElement('type');
1507+
$typeElement->setAttribute("class", "union");
1508+
1509+
foreach ($type->types as $type) {
1510+
$unionTypeElement = $doc->createElement('type', $type->name);
1511+
$typeElement->appendChild($unionTypeElement);
1512+
}
1513+
} else {
1514+
$typeElement = $doc->createElement('type', $type->types[0]->name);
1515+
}
1516+
1517+
$elementToAppend->appendChild($typeElement);
1518+
}
13351519
}
13361520

13371521
class FileInfo {
@@ -1593,12 +1777,15 @@ function parseProperty(
15931777
?DocComment $comment
15941778
): PropertyInfo {
15951779
$docType = false;
1780+
$isReadonly = false;
15961781

15971782
if ($comment) {
15981783
$tags = parseDocComment($comment);
15991784
foreach ($tags as $tag) {
16001785
if ($tag->name === 'var') {
16011786
$docType = true;
1787+
} elseif ($tag->name === 'readonly') {
1788+
$isReadonly = true;
16021789
}
16031790
}
16041791
}
@@ -1623,7 +1810,8 @@ function parseProperty(
16231810
new PropertyName($class, $property->name->__toString()),
16241811
$flags,
16251812
$propertyType,
1626-
$property->default
1813+
$property->default,
1814+
$isReadonly
16271815
);
16281816
}
16291817

@@ -2069,6 +2257,23 @@ function generateFunctionEntries(?Name $className, array $funcInfos): string {
20692257
return $code;
20702258
}
20712259

2260+
/**
2261+
* @param ClassInfo[] $classMap
2262+
* @return array<string, string>
2263+
*/
2264+
function generateClassSynopses(array $classMap): array {
2265+
$result = [];
2266+
2267+
foreach ($classMap as $classInfo) {
2268+
$classSynopsis = $classInfo->getClassSynopsisDocument($classMap);
2269+
if ($classSynopsis !== null) {
2270+
$result[$classInfo->getClassSynopsisFilename() . ".xml"] = $classSynopsis;
2271+
}
2272+
}
2273+
2274+
return $result;
2275+
}
2276+
20722277
/**
20732278
* @param FuncInfo[] $funcMap
20742279
* @param FuncInfo[] $aliasMap
@@ -2322,15 +2527,24 @@ function initPhpParser() {
23222527
}
23232528

23242529
$optind = null;
2325-
$options = getopt("fh", ["force-regeneration", "parameter-stats", "help", "verify", "generate-methodsynopses", "replace-methodsynopses"], $optind);
2530+
$options = getopt(
2531+
"fh",
2532+
[
2533+
"force-regeneration", "parameter-stats", "help", "verify", "generate-classsynopses", "replace-classsynopses",
2534+
"generate-methodsynopses", "replace-methodsynopses"
2535+
],
2536+
$optind
2537+
);
23262538

23272539
$context = new Context;
23282540
$printParameterStats = isset($options["parameter-stats"]);
23292541
$verify = isset($options["verify"]);
2542+
$generateClassSynopses = isset($options["generate-classsynopses"]);
2543+
$replaceClassSynopses = isset($options["replace-classsynopses"]);
23302544
$generateMethodSynopses = isset($options["generate-methodsynopses"]);
23312545
$replaceMethodSynopses = isset($options["replace-methodsynopses"]);
23322546
$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]);
2333-
$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $generateMethodSynopses || $replaceMethodSynopses;
2547+
$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $generateClassSynopses || $replaceClassSynopses || $generateMethodSynopses || $replaceMethodSynopses;
23342548
$targetMethodSynopses = $argv[$optind + 1] ?? null;
23352549
if ($replaceMethodSynopses && $targetMethodSynopses === null) {
23362550
die("A target directory must be provided.\n");
@@ -2375,19 +2589,28 @@ function initPhpParser() {
23752589
echo json_encode($parameterStats, JSON_PRETTY_PRINT), "\n";
23762590
}
23772591

2592+
/** @var ClassInfo[] $classMap */
2593+
$classMap = [];
23782594
/** @var FuncInfo[] $funcMap */
23792595
$funcMap = [];
23802596
/** @var FuncInfo[] $aliasMap */
23812597
$aliasMap = [];
23822598

23832599
foreach ($fileInfos as $fileInfo) {
2384-
foreach ($fileInfo->getAllFuncInfos() as $funcInfo) {
2385-
/** @var FuncInfo $funcInfo */
2386-
$funcMap[$funcInfo->name->__toString()] = $funcInfo;
2600+
foreach ($fileInfo->classInfos as $classInfo) {
2601+
$classMap[$classInfo->name->__toString()] = $classInfo;
23872602

2388-
// TODO: Don't use aliasMap for methodsynopsis?
2389-
if ($funcInfo->aliasType === "alias") {
2390-
$aliasMap[$funcInfo->alias->__toString()] = $funcInfo;
2603+
foreach ($fileInfo->funcInfos as $funcInfo) {
2604+
$funcMap[$funcInfo->name->__toString()] = $funcInfo;
2605+
}
2606+
2607+
foreach ($classInfo->funcInfos as $funcInfo) {
2608+
$funcMap[$funcInfo->name->__toString()] = $funcInfo;
2609+
2610+
// TODO: Don't use aliasMap for methodsynopsis?
2611+
if ($funcInfo->aliasType === "alias") {
2612+
$aliasMap[$funcInfo->alias->__toString()] = $funcInfo;
2613+
}
23912614
}
23922615
}
23932616
}
@@ -2468,6 +2691,23 @@ function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc
24682691
}
24692692
}
24702693

2694+
if ($generateClassSynopses) {
2695+
$classSynopsesDirectory = getcwd() . "/classsynopses";
2696+
2697+
$classSynopses = generateClassSynopses($classMap);
2698+
if (!empty($classSynopses)) {
2699+
if (!file_exists($classSynopsesDirectory)) {
2700+
mkdir($classSynopsesDirectory);
2701+
}
2702+
2703+
foreach ($classSynopses as $filename => $content) {
2704+
if (file_put_contents("$classSynopsesDirectory/$filename", $content)) {
2705+
echo "Saved $filename\n";
2706+
}
2707+
}
2708+
}
2709+
}
2710+
24712711
if ($generateMethodSynopses) {
24722712
$methodSynopsesDirectory = getcwd() . "/methodsynopses";
24732713

0 commit comments

Comments
 (0)