From 081cec2c9cfe879588d5989bcc09a34412f1d9c9 Mon Sep 17 00:00:00 2001 From: Michael Wallner Date: Wed, 19 Jan 2022 18:42:45 +0100 Subject: [PATCH] stub2ref --- bin/stub2ref | 30 +++++++++++ composer.json | 7 ++- mdref/Action.php | 7 ++- mdref/Entry.php | 2 +- mdref/Generator.php | 56 ++++++++++++++++++++ mdref/Generator/Arg.php | 35 ++++++++++++ mdref/Generator/Cls.php | 85 +++++++++++++++++++++++++++++ mdref/Generator/Func.php | 94 ++++++++++++++++++++++++++++++++ mdref/Generator/Param.php | 48 +++++++++++++++++ mdref/Generator/Prop.php | 37 +++++++++++++ mdref/Generator/Scrap.php | 79 +++++++++++++++++++++++++++ mdref/Generator/SeeAlso.php | 39 ++++++++++++++ mdref/Generator/Template.php | 37 +++++++++++++ mdref/Inspector.php | 100 +++++++++++++++++++++++++++++++++++ mdref/Tree.php | 6 +-- public/index.js | 2 + 16 files changed, 656 insertions(+), 8 deletions(-) create mode 100755 bin/stub2ref create mode 100644 mdref/Generator.php create mode 100644 mdref/Generator/Arg.php create mode 100644 mdref/Generator/Cls.php create mode 100644 mdref/Generator/Func.php create mode 100644 mdref/Generator/Param.php create mode 100644 mdref/Generator/Prop.php create mode 100644 mdref/Generator/Scrap.php create mode 100644 mdref/Generator/SeeAlso.php create mode 100644 mdref/Generator/Template.php create mode 100644 mdref/Inspector.php diff --git a/bin/stub2ref b/bin/stub2ref new file mode 100755 index 0000000..de1899b --- /dev/null +++ b/bin/stub2ref @@ -0,0 +1,30 @@ +#!/usr/bin/env php + && %s \n", $argv[0]); + exit(1); +} + +$namespace = $argv[1]; +require_once $argv[2]; + +if (!file_exists("$namespace.mdref")) { + fprintf(STDERR, "Missing $namespace.mdref; generated default.\n"); + file_put_contents("$namespace.mdref", "./%s"); +} +if (!file_exists("$namespace.md")) { + fprintf(STDERR, "Missing $namespace.md; hard linking README.md\n"); + link(dirname($argv[2]) . "/README.md", "$namespace.md"); +} + +$inspector = new Inspector; +$inspector->inspectNamespace($namespace); + +$generator = new Generator; +$generator->generateFunctions($inspector->getFunctions()); +$generator->generateClasses($inspector->getClasses()); diff --git a/composer.json b/composer.json index 01de530..4a49999 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,10 @@ } ], "bin": [ + "bin/ext2ref", + "bin/ref2html", "bin/ref2stub", - "bin/ref2html" + "bin/stub2ref" ], "autoload": { "psr-4": { @@ -23,7 +25,8 @@ "ext-filter": "*", "ext-pcre": "*", "ext-http": "^3.2 || ^4.2", - "league/commonmark": "^2.0" + "league/commonmark": "^2.0", + "phpdocumentor/reflection-docblock": "^5.3" }, "suggests": { "ext-discount": "A Markdown renderer is needed, this is the preferred one." diff --git a/mdref/Action.php b/mdref/Action.php index da3a4da..3addfa0 100644 --- a/mdref/Action.php +++ b/mdref/Action.php @@ -180,6 +180,9 @@ class Action { include ROOT."/views/layout.phtml"; $this->response->addHeader("Link", "<" . $this->baseUrl->path . "index.css>; rel=preload; as=style"); $this->response->addHeader("Link", "<" . $this->baseUrl->path . "index.js>; rel=preload; as=script"); + if (isset($exception) && $exception->getCode()) { + $this->response->setResponseCode($exception->getCode()); + } if (is_resource($this->output)) { $this->response->send($this->output); } else { @@ -194,10 +197,10 @@ class Action { try { $pld = $this->createPayload(); - if (strlen($pld->ref)) { + if (isset($pld->ref) && strlen($pld->ref)) { $cnn = null; if (($repo = $this->reference->getRepoForEntry($pld->ref, $cnn))) { - if (strlen($cnn)) { + if (isset($cnn) && strlen($cnn)) { /* redirect */ $this->serveCanonical($cnn); return; diff --git a/mdref/Entry.php b/mdref/Entry.php index 152ba47..345f3c7 100644 --- a/mdref/Entry.php +++ b/mdref/Entry.php @@ -293,7 +293,7 @@ class Entry implements IteratorAggregate { * @return bool */ function hasIterator(?string $glob = null, bool $loose = false) : bool { - if (strlen($glob)) { + if (isset($glob) && strlen($glob)) { return glob($this->getNextDirname() . "/$glob") || ($loose && glob($this->getNextDirname() . "/*/$glob")); } elseif ($this->isRoot()) { diff --git a/mdref/Generator.php b/mdref/Generator.php new file mode 100644 index 0000000..84a323e --- /dev/null +++ b/mdref/Generator.php @@ -0,0 +1,56 @@ +> $functions + * @return void + */ + public function generateFunctions(array $functions) : void { + foreach ($functions as $ns => $funcs) { + $ns_path = strtr($ns, "\\", "/"); + foreach ($funcs as $fn => $rf) { + $fn_file = "$ns_path/$fn.md"; + fprintf(STDERR, "Generating %s\n", $fn_file); + is_dir($ns_path) || mkdir($ns_path, 0770, true); + file_put_contents($fn_file, new Func($this, $rf)); + } + } + } + + /** + * @param array> $classes + * @return void + */ + public function generateClasses(array $classes) : void { + foreach ($classes as $ns => $cls) { + $ns_path = strtr($ns, "\\", "/"); + foreach ($cls as $cn => $rc) { + $cn_path = "$ns_path/$cn"; + $cn_file = "$cn_path.md"; + fprintf(STDERR, "Generating %s\n", $cn_file); + is_dir($ns_path) || mkdir($ns_path, 0770, true); + file_put_contents($cn_file, new Cls($this, $rc)); + $this->generateMethods($rc); + } + } + } + + private function generateMethods(\ReflectionClass $rc) : void { + $funcs = []; + foreach ($rc->getMethods(\ReflectionMethod::IS_PUBLIC) as $rm) { + if ($rm->getDeclaringClass()->getName() === $rc->getName()) { + foreach ($rc->getInterfaces() as $ri) { + if ($ri->hasMethod($rm->getName())) { + continue 2; + } + } + $funcs[$rc->getName()][$rm->getName()] = $rm; + } + } + $this->generateFunctions($funcs); + } +} diff --git a/mdref/Generator/Arg.php b/mdref/Generator/Arg.php new file mode 100644 index 0000000..df6fbbc --- /dev/null +++ b/mdref/Generator/Arg.php @@ -0,0 +1,35 @@ + $this->getParamTag($this->ref->getName()) + ]); + } + +} + +/** @var $gen Generator */ +/** @var $ref \ReflectionParameter */ +/** @var $doc ?DocBlock */ +/** @var $tag ?Tags\Param */ + +__HALT_COMPILER(); +hasType() ? $ref->getType() : str_replace("\\ref", "", $tag?->getType() ?? "mixed") +?> isVariadic()) : ?> + ?>...isPassedByReference()) : + ?>&$getName() +?>isDefaultValueAvailable()) : + ?> = getDefaultValue()) ?>enumgetModifiers())); + ?> isInterface() ? "interface" : "class" + ?> getName() ?>getParentClass())) : + ?> extends getName() ?>getInterfaceNames())) : sort($implements); + ?> implements + + +getSummary() ?> + + +getDescription() ?> + + + + + + +## Constants: + +getReflectionConstants(), fn($rc) => $rc->getDeclaringClass()->getName() === $ref->getName()))) : + ?>None. * getName(); + ?> = getValue() instanceof \UnitEnum) : + var_export($rc->getValue()->value); + else : + var_export($rc->getValue()); + endif; + ?>\n" + ?> + + +## Properties: + +getProperties(), fn($rp) => $rp->getDeclaringClass()->getName() === $ref->getName()))) : + ?>None. * + +isFinal()) : + ?>final isStatic()) : + ?>static hasReturnType() ? $ref->getReturnType() : "void" +?> getDeclaringClass()->getName() + ?>::getName() ?> +(getParameters() as $i => $param) : + if ($param->isOptional()) : $optional++ + ?>[getNumberOfParameters()-1): + ?>, +) + +getSummary() ?> + + +getDescription()?->getBodyTemplate() ?> + + + + + + +## Params: + +getParameters())) : + ?>None. $param) : + $patch(Param::class, $param); + endforeach; +endif; + +if (($tags = $doc?->getTagsWithTypeByName("return")) || ($ref->hasReturnType() && $ref->hasReturnType() != "void")) : +?> + + +## Returns: + +* getType() + ?>, getDescription() + ?>* getReturnType() + ?> + + + $this->getParamTag($this->ref->getName()) + ]); + } +} + +/** @var $gen Generator */ +/** @var $ref \ReflectionParameter */ +/** @var $doc ?DocBlock */ +/** @var $tag ?Tags\Param */ + +__HALT_COMPILER(); +* isOptional()) : + ?>Optional hasType() ? $ref->getType() : str_replace("\\ref", "Reference",$tag?->getType() ?? "mixed") +?> isVariadic()) : ?> + ?>...isPassedByReference()) : + ?>&$getName() +?>isDefaultValueAvailable()) : + ?> = getDefaultValue()) ?>getDescription() ?? $doc?->getSummary())) : + ?> + + $this->getVarTag($this->ref->getName()) + ]); + } + +} + +/** @var $gen Generator */ +/** @var $ref \ReflectionParameter */ +/** @var $doc ?DocBlock */ +/** @var $tag ?Tags\Param */ + +__HALT_COMPILER(); +getModifiers())) +?> hasType() ? $ref->getType() : ($tag?->getType() ?? "mixed") +?> $getName() ?>hasDefaultValue()) : + ?> = getDefaultValue()) ?>getSummary())) : + ?>doc) { + $this->createDocBlock(); + } + if (!$this->doc) { + fprintf(STDERR, ... match (get_class($ref)) { + \ReflectionClass::class => ["Missing docs for class %s\n", $ref->name], + \ReflectionMethod::class => ["Missing docs for method %s::%s()\n", $ref->class, $ref->name], + \ReflectionProperty::class => ["Missing docs for property %s %s::\$%s\n", $ref->getType(), $ref->class, $ref->name], + \ReflectionClassConstant::class => ["Missing docs for constant %s::%s\n", $ref->class, $ref->name], + \ReflectionFunction::class => ["Missing docs for function %s()\n", $ref->name], + \ReflectionParameter::class => ($ref->getDeclaringClass() + ? ["Missing docs for method arg %s::%s(%s $%s)\n", $ref->getDeclaringClass()->name] + : ["Missing docs for function arg %s(%s $%s)\n"]) + + [3=>$ref->getDeclaringFunction()->name, $ref->getType(), $ref->name], + default => ["Missing docs for ??? %s\n", $ref->name], + }); + } + } + + protected function createDocBlock() { + if (method_exists($this->ref, "getDocComment")) { + $docs = $this->ref->getDocComment(); + } elseif (($this->ref instanceof \ReflectionParameter) && $this->ref->getDeclaringClass()?->hasProperty($this->ref->name)) { + // ctor promoted properties + $docs = $this->ref->getDeclaringClass()->getProperty($this->ref->name)->getDocComment(); + } + if (isset($docs) && $docs !== false && strlen($docs)) { + $this->doc = DocBlockFactory::createInstance()->create((string) $docs); + } + } + + protected function toString(string $file, int $offset, array $imports = []) : string { + $tpl = (string) new Template($file, $offset); + $patch = function(string $scrap_class, Reflector $ref) : void { + echo new $scrap_class($this->gen, $ref, $this->doc, true); + }; + return (static function(Generator $gen, Reflector $ref, ?DocBlock $doc) use($patch) { + $imports = func_get_arg(3); extract($imports); unset($imports); + ob_start(null, 0x4000); + include func_get_arg(4); + return ob_get_clean(); + })($this->gen, $this->ref, $this->doc, $imports, $tpl); + } + + protected function getParamTag(string $var_name) : ?Tags\Param { + if ($this->doc) foreach ($this->doc->getTagsByName("param") as $param) { + if ($param->getVariableName() === $var_name) { + return $param; + } + } + return null; + } + + protected function getVarTag(string $var_name) : ?Tags\Var_ { + if ($this->doc) foreach ($this->doc->getTagsByName("var") as $prop) { + if ($prop->getVariableName() === $var_name) { + return $prop; + } + } + return null; + } +} diff --git a/mdref/Generator/SeeAlso.php b/mdref/Generator/SeeAlso.php new file mode 100644 index 0000000..69f7892 --- /dev/null +++ b/mdref/Generator/SeeAlso.php @@ -0,0 +1,39 @@ +getTagsByName("see"))) : + ?>See also $see) : + /** @var $see DocBlock\Tags\See */ + if (($desc = $see->getDescription())) : + ?>[getDescription() ?>](getReference() ?>)getReference() ?>., and file, false, null, $this->offset); + } + + private function cache(string $cache_file) : string { + $cache_path = dirname($cache_file); + if (!is_dir($cache_path) && !mkdir($cache_path, 0700, true)) { + throw new RuntimeException(error_get_last()["message"]); + } + if (!file_put_contents($cache_file, '' . $this->source())) { + throw new RuntimeException(error_get_last()["message"]); + } + return $cache_file; + } + + public function __toString() : string { + $cache_file = sys_get_temp_dir() . "/mdref/generate." . basename($this->file) . ".md.tmp"; + $cache_stat = @stat($cache_file); + if ($cache_stat && $cache_stat["mtime"] >= filemtime($this->file)) { + return $cache_file; + } + + return $this->cache($cache_file); + } +} diff --git a/mdref/Inspector.php b/mdref/Inspector.php new file mode 100644 index 0000000..162cc51 --- /dev/null +++ b/mdref/Inspector.php @@ -0,0 +1,100 @@ +addClasses($ext->getClasses()); + $this->addFunctions($ext->getFunctions()); + $this->addConstants($ext->getConstants()); + } + + public function inspectNamespace(string $namespace) { + $grep = function(array $a) use($namespace) { + return preg_grep("/^" . preg_quote($namespace) . "\\\\/", $a); + }; + + $this->addClasses($this->wrap(\ReflectionClass::class, $grep(get_declared_interfaces()))); + $this->addClasses($this->wrap(\ReflectionClass::class, array_filter( + $grep(get_declared_classes()), + fn($cn) => !is_subclass_of($cn, \UnitEnum::class) + ))); + $this->addClasses($this->wrap(\ReflectionEnum::class, array_filter( + $grep(get_declared_classes()), + fn($cn) => is_subclass_of($cn, \UnitEnum::class) + ))); + $this->addFunctions($this->wrap(\ReflectionFunction::class, $grep(get_defined_functions()["internal"]))); + $this->addFunctions($this->wrap(\ReflectionFunction::class, $grep(get_defined_functions()["user"]))); + $this->addConstants($grep(get_defined_constants())); + } + + private function wrap(string $klass, array $list) : array { + $res = []; + foreach ($list as $entry) { + $res[] = new $klass($entry); + } + return $res; + } + + private function addClasses(array $classes) { + foreach ($classes as $c) { + /* @var $c \ReflectionClass */ + $ns_name = $c->inNamespace() ? $c->getNamespaceName() : ""; + $this->classes[$ns_name][$c->getShortName()] = $c; + } + } + + private function addFunctions(array $functions) { + foreach ($functions as $f) { + /* @var $f \ReflectionFunction */ + $ns_name = $f->inNamespace() ? $f->getNamespaceName() : ""; + $this->functions[$ns_name][$f->getShortName()] = $f; + } + } + + private function addConstants(array $constants) { + foreach ($constants as $constant => $value) { + $ns_name = ($ns_end = strrpos($constant, "\\")) ? substr($constant, 0, $ns_end++) : ""; + $cn = substr($constant, $ns_end); + $this->constants[$ns_name][$cn] = $value; + } + } + + /** + * @return array + */ + public function getConstants() : array { + return $this->constants; + } + + /** + * @return array + */ + public function getClasses(): array { + return $this->classes; + } + + /** + * @return array + */ + public function getFunctions(): array { + return $this->functions; + } +} diff --git a/mdref/Tree.php b/mdref/Tree.php index fc12a1c..68c5113 100644 --- a/mdref/Tree.php +++ b/mdref/Tree.php @@ -92,7 +92,7 @@ class Tree implements RecursiveIterator { } elseif ($bb[0] === ":") { return 1; } - +/* $ad = is_dir(dirname($a)."/$ab"); $bd = is_dir(dirname($b)."/$bb"); @@ -103,7 +103,7 @@ class Tree implements RecursiveIterator { } elseif ($bd) { return 1; } - +*/ $au = preg_match("/^\p{Lu}/", $ab); $bu = preg_match("/^\p{Lu}/", $bb); @@ -170,7 +170,7 @@ class Tree implements RecursiveIterator { * Implements \RecursiveIterator * @return \mdref\Tree */ - public function getChildren() : Iterator { + public function getChildren() : Tree { return $this->current()->getIterator(); } } diff --git a/public/index.js b/public/index.js index 45862d0..5124ffe 100644 --- a/public/index.js +++ b/public/index.js @@ -51,6 +51,7 @@ $(function() { case "Traversable": case "Iterator": case "IteratorAggregate": + case "RecursiveIterator": case "ArrayAccess": case "ArrayObject": case "ArrayIterator": @@ -73,6 +74,7 @@ $(function() { return "
 "; } case "class": + case "enum": case "interface": case "namespace": case "public": -- 2.30.2