├── src └── voku │ └── SimplePhpParser │ ├── Parsers │ ├── Helper │ │ ├── DocFactoryProvider.php │ │ ├── ParserErrorHandler.php │ │ ├── ParserContainer.php │ │ └── Utils.php │ ├── Visitors │ │ ├── ParentConnector.php │ │ └── ASTVisitor.php │ └── PhpCodeParser.php │ └── Model │ ├── BasePHPClass.php │ ├── PHPDefineConstant.php │ ├── BasePHPElement.php │ ├── PHPDocElement.php │ ├── PHPConst.php │ ├── PHPInterface.php │ ├── PHPMethod.php │ ├── PHPProperty.php │ ├── PHPFunction.php │ ├── PHPParameter.php │ ├── PHPTrait.php │ └── PHPClass.php ├── composer.json ├── CHANGELOG.md ├── README.md └── LICENSE /src/voku/SimplePhpParser/Parsers/Helper/DocFactoryProvider.php: -------------------------------------------------------------------------------- 1 | setRawMessage($error->getRawMessage() . "\n" . $error->getFile()); 20 | 21 | parent::handleError($error); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/BasePHPClass.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public array $methods = []; 15 | 16 | /** 17 | * @var array 18 | */ 19 | public array $properties = []; 20 | 21 | /** 22 | * @var array 23 | */ 24 | public array $constants = []; 25 | 26 | public ?bool $is_final = null; 27 | 28 | public ?bool $is_abstract = null; 29 | 30 | public ?bool $is_readonly = null; 31 | 32 | public ?bool $is_anonymous = null; 33 | 34 | public ?bool $is_cloneable = null; 35 | 36 | public ?bool $is_instantiable = null; 37 | 38 | public ?bool $is_iterable = null; 39 | } 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voku/simple-php-code-parser", 3 | "description": "Get a simple data structure from your php code.", 4 | "type": "library", 5 | "keywords": [ 6 | "php", 7 | "parser", 8 | "phpdoc" 9 | ], 10 | "homepage": "https://github.com/voku/Simple-PHP-Code-Parser", 11 | "license": "Apache-2.0", 12 | "authors": [ 13 | { 14 | "name": "Lars Moelleken", 15 | "homepage": "https://www.moelleken.org/" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.4", 20 | "react/async": "~3.0.0 || ~4.1.0", 21 | "react/filesystem": "^0.2@dev", 22 | "phpdocumentor/type-resolver": "~1.7.2", 23 | "phpdocumentor/reflection-docblock": "~5.3", 24 | "phpdocumentor/reflection-common": "~2.2", 25 | "phpstan/phpdoc-parser": "~1.23", 26 | "voku/simple-cache": "~4.1", 27 | "nikic/php-parser": "~4.16" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "voku\\": "src/voku/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "voku\\tests\\": "tests/" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Parsers/Visitors/ParentConnector.php: -------------------------------------------------------------------------------- 1 | stack = []; 23 | } 24 | 25 | /** 26 | * @param \PhpParser\Node[] $nodes 27 | * 28 | * @return null 29 | */ 30 | public function beforeTraverse(array $nodes) 31 | { 32 | $this->stack = []; 33 | 34 | return null; 35 | } 36 | 37 | /** 38 | * @param \PhpParser\Node $node 39 | * 40 | * @return int|\PhpParser\Node|null 41 | */ 42 | public function enterNode(Node $node) 43 | { 44 | $stackCount = \count($this->stack); 45 | if ($stackCount > 0) { 46 | $node->setAttribute('parent', $this->stack[$stackCount - 1]); 47 | } 48 | 49 | $this->stack[] = $node; 50 | 51 | return $node; 52 | } 53 | 54 | /** 55 | * @param \PhpParser\Node $node 56 | * 57 | * @return null 58 | */ 59 | public function leaveNode(Node $node) 60 | { 61 | \array_pop($this->stack); 62 | 63 | return null; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPDefineConstant.php: -------------------------------------------------------------------------------- 1 | prepareNode($node); 21 | 22 | if ( 23 | isset($node->args[0]) 24 | && 25 | \property_exists($node->args[0], 'value') 26 | && 27 | \property_exists($node->args[0]->value, 'value') 28 | ) { 29 | $constName = $this->getConstantFQN($node, (string)$node->args[0]->value->value); 30 | } else { 31 | $constName = ''; 32 | } 33 | 34 | if (\in_array($constName, ['null', 'true', 'false'], true)) { 35 | $constName = \strtoupper($constName); 36 | } 37 | 38 | $this->name = $constName; 39 | 40 | /* @phpstan-ignore-next-line */ 41 | $this->value = Utils::getPhpParserValueFromNode($node->args[1]); 42 | 43 | $this->type = Utils::normalizePhpType(\gettype($this->value)); 44 | 45 | $this->collectTags($node); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @param \ReflectionClassConstant $constant 52 | * 53 | * @return $this 54 | */ 55 | public function readObjectFromReflection($constant): PHPConst 56 | { 57 | $this->name = $constant->getName(); 58 | $this->value = $constant->getValue(); 59 | $this->type = Utils::normalizePhpType(\gettype($this->value)); 60 | 61 | return $this; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/BasePHPElement.php: -------------------------------------------------------------------------------- 1 | parserContainer = $parserContainer; 33 | } 34 | 35 | /** 36 | * @param \Reflector $object 37 | * 38 | * @return $this 39 | */ 40 | abstract public function readObjectFromReflection($object); 41 | 42 | /** 43 | * @param \PhpParser\NodeAbstract $mixed_1 44 | * @param \PhpParser\NodeAbstract|null $mixed_2 45 | * 46 | * @return $this 47 | */ 48 | abstract public function readObjectFromPhpNode($mixed_1, $mixed_2 = null); 49 | 50 | protected function getConstantFQN(NodeAbstract $node, string $nodeName): string 51 | { 52 | $parent = $node->getAttribute('parent'); 53 | if ( 54 | $parent instanceof Namespace_ 55 | && 56 | $parent->name instanceof Name 57 | ) { 58 | $namespace = '\\' . \implode('\\', $parent->name->getParts()) . '\\'; 59 | } else { 60 | $namespace = ''; 61 | } 62 | 63 | return $namespace . $nodeName; 64 | } 65 | 66 | /** 67 | * @param \PhpParser\Node|string $node 68 | * 69 | * @return string 70 | * 71 | * @psalm-return class-string 72 | */ 73 | protected static function getFQN($node): string 74 | { 75 | // init 76 | $fqn = ''; 77 | 78 | if ( 79 | $node instanceof \PhpParser\Node 80 | && 81 | \property_exists($node, 'namespacedName') 82 | ) { 83 | if ($node->namespacedName) { 84 | $fqn = \implode('\\', $node->namespacedName->getParts()); 85 | } elseif (\property_exists($node, 'name') && $node->name) { 86 | var_dump($node->name); 87 | $fqn = $node->name->name; 88 | } 89 | } 90 | 91 | /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */ 92 | /** @var class-string $fqn */ 93 | $fqn = $fqn; 94 | 95 | return $fqn; 96 | } 97 | 98 | protected function prepareNode(Node $node): void 99 | { 100 | $this->line = $node->getLine(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPDocElement.php: -------------------------------------------------------------------------------- 1 | 51 | */ 52 | public array $tagNames = []; 53 | 54 | public bool $hasLinkTag = false; 55 | 56 | public bool $hasSeeTag = false; 57 | 58 | public bool $hasSinceTag = false; 59 | 60 | public bool $hasDeprecatedTag = false; 61 | 62 | public bool $hasMetaTag = false; 63 | 64 | /** 65 | * @var bool 66 | */ 67 | public bool $hasInternalTag = false; 68 | 69 | public bool $hasRemovedTag = false; 70 | 71 | protected function collectTags(Node $node): void 72 | { 73 | $docComment = $node->getDocComment(); 74 | if ($docComment) { 75 | try { 76 | $phpDoc = DocFactoryProvider::getDocFactory()->create((string)$docComment->getText()); 77 | 78 | $tags = $phpDoc->getTags(); 79 | foreach ($tags as $tag) { 80 | $this->tagNames[$tag->getName()] = $tag->render(); 81 | } 82 | 83 | $this->linkTags = $phpDoc->getTagsByName('link'); 84 | $this->seeTags = $phpDoc->getTagsByName('see'); 85 | $this->sinceTags = $phpDoc->getTagsByName('since'); 86 | $this->deprecatedTags = $phpDoc->getTagsByName('deprecated'); 87 | $this->metaTags = $phpDoc->getTagsByName('meta'); 88 | $this->internalTags = $phpDoc->getTagsByName('internal'); 89 | $this->removedTags = $phpDoc->getTagsByName('removed'); 90 | 91 | $this->hasLinkTag = $phpDoc->hasTag('link'); 92 | $this->hasSeeTag = $phpDoc->hasTag('see'); 93 | $this->hasSinceTag = $phpDoc->hasTag('since'); 94 | $this->hasDeprecatedTag = $phpDoc->hasTag('deprecated'); 95 | $this->hasMetaTag = $phpDoc->hasTag('meta'); 96 | $this->hasInternalTag = $phpDoc->hasTag('internal'); 97 | $this->hasRemovedTag = $phpDoc->hasTag('removed'); 98 | } catch (\Exception $e) { 99 | $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true); 100 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPConst.php: -------------------------------------------------------------------------------- 1 | |null 25 | */ 26 | public $value; 27 | 28 | public ?string $visibility = null; 29 | 30 | public ?string $type = null; 31 | 32 | /** 33 | * @param Const_ $node 34 | * @param null $dummy 35 | * 36 | * @return $this 37 | */ 38 | public function readObjectFromPhpNode($node, $dummy = null): self 39 | { 40 | $this->prepareNode($node); 41 | 42 | $this->name = $this->getConstantFQN($node, $node->name->name); 43 | 44 | $this->value = Utils::getPhpParserValueFromNode($node); 45 | 46 | $this->type = Utils::normalizePhpType(\gettype($this->value)); 47 | 48 | $parentNode = $node->getAttribute('parent'); 49 | 50 | if ($parentNode instanceof ClassConst) { 51 | if ($parentNode->isPrivate()) { 52 | $this->visibility = 'private'; 53 | } elseif ($parentNode->isProtected()) { 54 | $this->visibility = 'protected'; 55 | } else { 56 | $this->visibility = 'public'; 57 | } 58 | 59 | $this->parentName = self::getFQN($parentNode->getAttribute('parent')); 60 | } 61 | 62 | $this->collectTags($node); 63 | 64 | if ($node->getAttribute('parent') instanceof ClassConst) { 65 | $this->parentName = static::getFQN($node->getAttribute('parent')->getAttribute('parent')); 66 | } 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * @param ReflectionClassConstant $constant 73 | * 74 | * @return $this 75 | */ 76 | public function readObjectFromReflection($constant): self 77 | { 78 | $this->name = $constant->getName(); 79 | 80 | $file = $constant->getDeclaringClass()->getFileName(); 81 | if ($file) { 82 | $this->file = $file; 83 | } 84 | 85 | /** @psalm-suppress InvalidPropertyAssignmentValue - upstream phpdoc error ? */ 86 | $this->value = $constant->getValue(); 87 | 88 | $this->type = \gettype($this->value); 89 | 90 | if ($constant->isPrivate()) { 91 | $this->visibility = 'private'; 92 | } elseif ($constant->isProtected()) { 93 | $this->visibility = 'protected'; 94 | } else { 95 | $this->visibility = 'public'; 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | protected function getConstantFQN(NodeAbstract $node, string $nodeName): string 102 | { 103 | $parent = $node->getAttribute('parent'); 104 | $parentParentNode = $parent ? $parent->getAttribute('parent') : null; 105 | 106 | if ( 107 | $parentParentNode instanceof Namespace_ 108 | && 109 | $parentParentNode->name instanceof Name 110 | ) { 111 | $namespace = '\\' . \implode('\\', $parentParentNode->name->getParts()) . '\\'; 112 | } else { 113 | $namespace = ''; 114 | } 115 | 116 | return $namespace . $nodeName; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPInterface.php: -------------------------------------------------------------------------------- 1 | prepareNode($node); 34 | 35 | $this->name = static::getFQN($node); 36 | 37 | $interfaceExists = false; 38 | try { 39 | if (\interface_exists($this->name, true)) { 40 | $interfaceExists = true; 41 | } 42 | } catch (\Exception $e) { 43 | // nothing 44 | } 45 | if ($interfaceExists) { 46 | $reflectionInterface = Utils::createClassReflectionInstance($this->name); 47 | $this->readObjectFromReflection($reflectionInterface); 48 | } 49 | 50 | $this->collectTags($node); 51 | 52 | foreach ($node->getMethods() as $method) { 53 | $methodNameTmp = $method->name->name; 54 | 55 | if (isset($this->methods[$methodNameTmp])) { 56 | $this->methods[$methodNameTmp] = $this->methods[$methodNameTmp]->readObjectFromPhpNode($method); 57 | } else { 58 | $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromPhpNode($method); 59 | } 60 | 61 | if (!$this->methods[$methodNameTmp]->file) { 62 | $this->methods[$methodNameTmp]->file = $this->file; 63 | } 64 | } 65 | 66 | if (!empty($node->extends)) { 67 | /** @var class-string $interfaceExtended */ 68 | $interfaceExtended = \implode('\\', $node->extends[0]->getParts()); 69 | $this->parentInterfaces[] = $interfaceExtended; 70 | } 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @param ReflectionClass $interface 77 | * 78 | * @return $this 79 | */ 80 | public function readObjectFromReflection($interface): self 81 | { 82 | $this->name = $interface->getName(); 83 | 84 | if (!$this->line) { 85 | $lineTmp = $interface->getStartLine(); 86 | if ($lineTmp !== false) { 87 | $this->line = $lineTmp; 88 | } 89 | } 90 | 91 | $file = $interface->getFileName(); 92 | if ($file) { 93 | $this->file = $file; 94 | } 95 | 96 | $this->is_final = $interface->isFinal(); 97 | 98 | $this->is_abstract = $interface->isAbstract(); 99 | 100 | $this->is_anonymous = $interface->isAnonymous(); 101 | 102 | $this->is_cloneable = $interface->isCloneable(); 103 | 104 | $this->is_instantiable = $interface->isInstantiable(); 105 | 106 | $this->is_iterable = $interface->isIterable(); 107 | 108 | foreach ($interface->getMethods() as $method) { 109 | $this->methods[$method->getName()] = (new PHPMethod($this->parserContainer))->readObjectFromReflection($method); 110 | } 111 | 112 | /** @var class-string[] $interfaceNames */ 113 | $interfaceNames = $interface->getInterfaceNames(); 114 | $this->parentInterfaces = $interfaceNames; 115 | 116 | foreach ($this->parentInterfaces as $parentInterface) { 117 | $interfaceExists = false; 118 | try { 119 | if ( 120 | !$this->parserContainer->getInterface($parentInterface) 121 | && 122 | \interface_exists($parentInterface, true) 123 | ) { 124 | $interfaceExists = true; 125 | } 126 | } catch (\Exception $e) { 127 | // nothing 128 | } 129 | if ($interfaceExists) { 130 | $reflectionInterface = Utils::createClassReflectionInstance($parentInterface); 131 | $parentInterfaceNew = (new self($this->parserContainer))->readObjectFromReflection($reflectionInterface); 132 | $this->parserContainer->addInterface($parentInterfaceNew); 133 | } 134 | } 135 | 136 | foreach ($interface->getReflectionConstants() as $constant) { 137 | $this->constants[$constant->getName()] = (new PHPConst($this->parserContainer))->readObjectFromReflection($constant); 138 | } 139 | 140 | return $this; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.20.1 (2023-08-11) 4 | 5 | - use "phpstan/phpdoc-parser" for more phpdocs 6 | 7 | ### 0.20.0 (2023-07-31) 8 | 9 | - add support for "readonly" properties 10 | - replace deprecated "parts" property from nikic/PHP-Parser 11 | - use "phpstan/phpdoc-parser" as fallback for parameters e.g. for `callable()` types 12 | - use php types 13 | - add more tests 14 | 15 | 16 | ### 0.19.6 (2022-09-23) 17 | 18 | - try to fix autoload #11 19 | - inheritDoc: use all types (not only from phpdoc) from parent classes & interfaces 20 | - add more tests 21 | 22 | 23 | ### 0.19.5 (2022-08-30) 24 | 25 | - support "::class" from PHP 8 26 | 27 | 28 | ### 0.19.4 (2022-08-30) 29 | 30 | - fix fatal error for non existing classes 31 | 32 | 33 | ### 0.19.3 (2022-08-27) 34 | 35 | - update "phpdocumentor/type-resolver" (with support for int-ranges) 36 | - clean-up some errors reported by phpstan 37 | 38 | 39 | ### 0.19.2 (2022-02-15) 40 | 41 | - quick-fix for missing namespaces in types 42 | 43 | 44 | ### 0.19.1 (2022-02-15) 45 | 46 | - optimize NULL detecting from reflection for types 47 | - "BasePHPClass" -> add more information from reflection 48 | - "PHPConst" -> add "visibility" 49 | 50 | 51 | ### 0.19.0 (2022-02-02) 52 | 53 | - use native php reflection, instead of (slower) better reflection 54 | - remove "react/filesystem" (not working with phar files) 55 | 56 | ### 0.18.2 (2021-11-29) 57 | 58 | - temporary fix for phpdoc like `int<0,1>` 59 | - update dependencies 60 | 61 | ### 0.18.1 (2021-10-22) 62 | 63 | - update dependencies 64 | 65 | ### 0.18.0 (2021-10-03) 66 | 67 | - update dependencies 68 | 69 | ### 0.17.0 (2021-07-27) 70 | 71 | - update dependencies 72 | 73 | ### 0.16.6 (2020-12-26) 74 | 75 | - "PhpCodeParser" -> optimize exception handling of "amphp/parallel" for async code analyse per file 76 | 77 | ### 0.16.5 (2020-12-26) 78 | 79 | - "PhpCodeParser" -> ignore exceptions from auto loaded external classes 80 | 81 | ### 0.16.4 (2020-11-18) 82 | 83 | - "PhpCodeParser" -> allow different file extensions 84 | 85 | ### 0.16.3 (2020-10-31) 86 | 87 | - fix php variable detection 88 | 89 | ### 0.16.2 (2020-10-30) 90 | 91 | - save the raw phpdoc for "@return" and "@param" v2 92 | 93 | ### 0.16.1 (2020-10-30) 94 | 95 | - save the raw phpdoc for "@return" and "@param" 96 | 97 | ### 0.16.0 (2020-10-30) 98 | 99 | - support for modern phpdocs for "@param" and "@return" 100 | 101 | ### 0.15.3 (2020-10-06) 102 | 103 | - update vendor libs 104 | - PHP 7.2 as minimal requirement 105 | - remove custom PseudoTypes for phpDocumentor (PseudoTypes are now build in into phpDocumentor itself) 106 | - add more tests for "PhpCodeParser->getFunctionsInfo()" 107 | 108 | ### 0.15.2 (2020-09-04) 109 | 110 | - save special phpdoc @tags and the content of these tags 111 | 112 | ### 0.15.1 (2020-09-04) 113 | 114 | - optimize performance via static cache 115 | 116 | ### 0.15.0 (2020-08-31) 117 | 118 | - move "PhpCodeChecker" in a separate repository 119 | - move "PhpCodeDumpApi" in a separate repository 120 | - clean-up vendor dependencies 121 | 122 | ### 0.14.0 (2020-08-23) 123 | 124 | - "PhpCodeCheckerCommand" -> allow to exclude some files 125 | - optimize parallel execution (cpu cores === max parallel executions) 126 | - update vendor (phpdocumentor & better-reflection) 127 | 128 | ### 0.13.2 (2020-08-07) 129 | 130 | - "PhpCodeParser" -> add "getFromClassName()" 131 | 132 | ### 0.13.1 (2020-07-21) 133 | 134 | - "PhpCodeParser" -> fix for inheritdoc comments 135 | 136 | ### 0.13.0 (2020-07-16) 137 | 138 | - "PHPTrait" -> add support for "Trait"-files 139 | 140 | ### 0.12.0 (2020-07-05) 141 | 142 | - "PhpCodeParser" -> fix cache key 143 | - "PhpCodeChecker" -> fix the autoloader parameter 144 | 145 | ### 0.11.0 (2020-06-18) 146 | 147 | - "PHPInterface" -> fix recursion 148 | - "PhpCodeParser" -> analyse only ".php" files in the given path 149 | 150 | ### 0.10.0 (2020-06-18) 151 | 152 | - "PhpCodeParser" -> ignore errors outside the current file-path-scope 153 | - "PhpCodeParser" -> use more generic autoloader logic 154 | - "PhpCodeChecker" -> fix more inheritdoc errors 155 | 156 | ### 0.9.0 (2020-06-16) 157 | 158 | - "PhpCodeChecker" -> check wrong phpdocs from class properties 159 | - "PhpCodeParser" -> allow to exclude some files 160 | 161 | ### 0.8.0 (2020-06-16) 162 | 163 | - replace PhpReflection with BetterReflection v2 164 | - fix bugs reported by PhpStan & PhpCodeCheck 165 | 166 | ### 0.7.0 (2020-06-02) 167 | 168 | - replace PhpReflection with BetterReflection 169 | 170 | ### 0.6.0 (2020-05-25) 171 | 172 | - "PhpCodeParser" -> fetch phpdoc-data for @inheritdoc 173 | 174 | ### 0.5.0 (2020-05-25) 175 | 176 | - "ParserErrorHandler" -> show more parsing errors in the results 177 | - "PHPInterface" -> fix PhpReflection usage 178 | - "PHPDefineConstant" -> fix php warning 179 | 180 | ### 0.4.2 (2020-05-23) 181 | 182 | - "PhpCodeChecker" -> fix "$skipMixedTypesAsError" usage 183 | 184 | ### 0.4.1 (2020-05-23) 185 | 186 | - "PHPFunction" -> fix phpdoc parsing error 187 | 188 | ### 0.4.0 (2020-05-23) 189 | 190 | - add default values for parameter and properties 191 | - add types from default values 192 | - normalize types (e.g. double => float) 193 | 194 | ### 0.3.0 (2020-05-20) 195 | 196 | - "PHPClass" fix phpdoc 197 | - add "PhpCodeChecker"-class 198 | 199 | ### 0.2.1 (2020-05-20) 200 | 201 | - fix code issues reported by psalm + phpstan 202 | 203 | ### 0.2.0 (2020-05-19) 204 | 205 | - use "amphp/parallel" for async code analyse per file 206 | 207 | ### 0.1.0 (2020-05-14) 208 | 209 | - init (forked from "JetBrains/phpstorm-stubs") 210 | - add "PHPProperties" 211 | - add "PHP Reflection" AND / OR "nikic/PHP-Parser" 212 | - get phpdocs types via phpDocumentor (+ psalm) 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/voku/Simple-PHP-Code-Parser/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/voku/Simple-PHP-Code-Parser/actions) 2 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/2feaf2a179a24a5fac99cbf67e72df2f)](https://www.codacy.com/manual/voku/Simple-PHP-Code-Parser?utm_source=github.com&utm_medium=referral&utm_content=voku/Simple-PHP-Code-Parser&utm_campaign=Badge_Grade) 3 | [![Latest Stable Version](https://poser.pugx.org/voku/Simple-PHP-Code-Parser/v/stable)](https://packagist.org/packages/voku/simple-php-code-parser) 4 | [![Total Downloads](https://poser.pugx.org/voku/simple-php-code-parser/downloads)](https://packagist.org/packages/voku/simple-php-code-parser) 5 | [![License](https://poser.pugx.org/voku/simple-php-code-parser/license)](https://packagist.org/packages/voku/simple-php-code-parser) 6 | [![Donate to this project using Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.me/moelleken) 7 | [![Donate to this project using Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/voku) 8 | 9 | # ❤ Simple PHP Code Parser 10 | 11 | You can simply scan a string, a file or a full directory and you can see a simple data structure from your php code. 12 | - Classes (**PHPClass**) 13 | - Properties (**PHPProperties**) 14 | - Constants (**PHPConst**) 15 | - Methods (**PHPMethod**) 16 | - Interfaces (**PHPInterface**) 17 | - Traits (**PHPTrait**) 18 | - Functions (**PHPFunction**) 19 | - Parameter (**PHPParameter**) 20 | 21 | This code is forked from [JetBrains/phpstorm-stubs](https://github.com/JetBrains/phpstorm-stubs/tree/master/tests) but you can't use the classes from "phpstorm-stubs" directly, 22 | because they are in a test namespace and the autoloader is "autoload-dev", so here is a extended version. 23 | 24 | We will use: 25 | - [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser) 26 | - [Reflection](https://www.php.net/manual/en/book.reflection.php) 27 | - [phpDocumentor](https://github.com/phpDocumentor/) 28 | - [PHPStan/phpdoc-parser](https://github.com/phpstan/phpdoc-parser) 29 | 30 | 31 | ### Install via "composer require" 32 | 33 | ```shell 34 | composer require voku/simple-php-code-parser 35 | ``` 36 | 37 | ### Quick Start 38 | 39 | Parse a string: 40 | ```php 41 | $code = ' 42 | getClasses(); 51 | 52 | var_dump($phpClasses['voku\tests\SimpleClass']); // "PHPClass"-object 53 | ``` 54 | 55 | Parse one class: 56 | ```php 57 | $phpCode = \voku\SimplePhpParser\Parsers\PhpCodeParser::getFromClassName(Dummy::class); 58 | $phpClasses = $phpCode->getClasses(); 59 | 60 | var_dump($phpClasses[Dummy::class]); // "PHPClass"-object 61 | 62 | var_dump($phpClasses[Dummy::class]->methods); // "PHPMethod[]"-objects 63 | 64 | var_dump($phpClasses[Dummy::class]->methods['withoutPhpDocParam']); // "PHPMethod"-object 65 | 66 | var_dump($phpClasses[Dummy::class]->methods['withoutPhpDocParam']->parameters); // "PHPParameter[]"-objects 67 | 68 | var_dump($phpClasses[Dummy::class]->methods['withoutPhpDocParam']->parameters['useRandInt']); // "PHPParameter"-object 69 | 70 | var_dump($phpClasses[Dummy::class]->methods['withoutPhpDocParam']->parameters['useRandInt']->type); // "bool" 71 | ```` 72 | 73 | Parse one file: 74 | ```php 75 | $phpCode = \voku\SimplePhpParser\Parsers\PhpCodeParser::getPhpFiles(__DIR__ . '/Dummy.php'); 76 | $phpClasses = $phpCode->getClasses(); 77 | 78 | var_dump($phpClasses[Dummy::class]); // "PHPClass"-object 79 | 80 | var_dump($phpClasses[Dummy::class]->methods); // "PHPMethod[]"-objects 81 | 82 | var_dump($phpClasses[Dummy::class]->methods['withoutPhpDocParam']); // "PHPMethod"-object 83 | 84 | var_dump($phpClasses[Dummy::class]->methods['withoutPhpDocParam']->parameters); // "PHPParameter[]"-objects 85 | 86 | var_dump($phpClasses[Dummy::class]->methods['withoutPhpDocParam']->parameters['useRandInt']); // "PHPParameter"-object 87 | 88 | var_dump($phpClasses[Dummy::class]->methods['withoutPhpDocParam']->parameters['useRandInt']->type); // "bool" 89 | ```` 90 | 91 | Parse many files: 92 | ```php 93 | $phpCode = \voku\SimplePhpParser\Parsers\PhpCodeParser::getPhpFiles(__DIR__ . '/src'); 94 | $phpClasses = $phpCode->getClasses(); 95 | 96 | var_dump($phpClasses[Dummy::class]); // "PHPClass"-object 97 | ```` 98 | 99 | 100 | ### Support 101 | 102 | For support and donations please visit [Github](https://github.com/voku/simple_html_dom/) | [Issues](https://github.com/voku/simple_html_dom/issues) | [PayPal](https://paypal.me/moelleken) | [Patreon](https://www.patreon.com/voku). 103 | 104 | For status updates and release announcements please visit [Releases](https://github.com/voku/simple_html_dom/releases) | [Twitter](https://twitter.com/suckup_de) | [Patreon](https://www.patreon.com/voku/posts). 105 | 106 | For professional support please contact [me](https://about.me/voku). 107 | 108 | ### Thanks 109 | 110 | - Thanks to [GitHub](https://github.com) (Microsoft) for hosting the code and a good infrastructure including Issues-Managment, etc. 111 | - Thanks to [IntelliJ](https://www.jetbrains.com) as they make the best IDEs for PHP and they gave me an open source license for PhpStorm! 112 | - Thanks to [Travis CI](https://travis-ci.com/) for being the most awesome, easiest continous integration tool out there! 113 | - Thanks to [StyleCI](https://styleci.io/) for the simple but powerfull code style check. 114 | - Thanks to [PHPStan](https://github.com/phpstan/phpstan) && [Psalm](https://github.com/vimeo/psalm) for really great Static analysis tools and for discover bugs in the code! 115 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Parsers/Visitors/ASTVisitor.php: -------------------------------------------------------------------------------- 1 | parserContainer = $parserContainer; 42 | } 43 | 44 | /** 45 | * @param \PhpParser\Node $node 46 | * 47 | * @return int|\PhpParser\Node|null 48 | */ 49 | public function enterNode(Node $node) 50 | { 51 | switch (true) { 52 | case $node instanceof Function_: 53 | 54 | $function = (new PHPFunction($this->parserContainer))->readObjectFromPhpNode($node); 55 | if (!$function->file) { 56 | $function->file = $this->fileName; 57 | } 58 | $this->parserContainer->addFunction($function); 59 | 60 | break; 61 | 62 | case $node instanceof Const_: 63 | 64 | $constant = new PHPConst($this->parserContainer); 65 | $constant = $constant->readObjectFromPhpNode($node); 66 | if (!$constant->file) { 67 | $constant->file = $this->fileName; 68 | } 69 | if ($constant->parentName === null) { 70 | $this->parserContainer->addConstant($constant); 71 | } elseif (($phpCodeParentConstantName = $this->parserContainer->getClass($constant->parentName)) !== null) { 72 | $phpCodeParentConstantName->constants[$constant->name] = $constant; 73 | } else { 74 | $interface = $this->parserContainer->getInterface($constant->parentName); 75 | if ($interface) { 76 | $interface->constants[$constant->name] = $constant; 77 | } 78 | } 79 | 80 | break; 81 | 82 | case $node instanceof FuncCall: 83 | 84 | if ( 85 | $node->name instanceof Node\Name 86 | && 87 | $node->name->getParts()[0] === 'define' 88 | ) { 89 | $constant = new PHPDefineConstant($this->parserContainer); 90 | $constant = $constant->readObjectFromPhpNode($node); 91 | if (!$constant->file) { 92 | $constant->file = $this->fileName; 93 | } 94 | $this->parserContainer->addConstant($constant); 95 | } 96 | 97 | break; 98 | 99 | case $node instanceof Interface_: 100 | 101 | $interface = (new PHPInterface($this->parserContainer))->readObjectFromPhpNode($node); 102 | if (!$interface->file) { 103 | $interface->file = $this->fileName; 104 | } 105 | $this->parserContainer->addInterface($interface); 106 | 107 | break; 108 | 109 | case $node instanceof Trait_: 110 | 111 | $trait = new PHPTrait($this->parserContainer); 112 | $trait = $trait->readObjectFromPhpNode($node); 113 | if (!$trait->file) { 114 | $trait->file = $this->fileName; 115 | } 116 | $this->parserContainer->addTrait($trait); 117 | 118 | break; 119 | 120 | case $node instanceof Class_: 121 | 122 | $class = new PHPClass($this->parserContainer); 123 | $class = $class->readObjectFromPhpNode($node); 124 | if (!$class->file) { 125 | $class->file = $this->fileName; 126 | } 127 | $this->parserContainer->addClass($class); 128 | 129 | break; 130 | 131 | default: 132 | 133 | // DEBUG 134 | //\var_dump($node); 135 | 136 | break; 137 | } 138 | 139 | return $node; 140 | } 141 | 142 | /** 143 | * @param \voku\SimplePhpParser\Model\PHPInterface $interface 144 | * 145 | * @return string[] 146 | * 147 | * @psalm-return class-string[] 148 | */ 149 | public function combineParentInterfaces($interface): array 150 | { 151 | // init 152 | $parents = []; 153 | 154 | if (empty($interface->parentInterfaces)) { 155 | return $parents; 156 | } 157 | 158 | foreach ($interface->parentInterfaces as $parentInterface) { 159 | $parents[] = $parentInterface; 160 | 161 | $phpCodeParentInterfaces = $this->parserContainer->getInterface($parentInterface); 162 | if ($phpCodeParentInterfaces !== null) { 163 | foreach ($this->combineParentInterfaces($phpCodeParentInterfaces) as $value) { 164 | $parents[] = $value; 165 | } 166 | } 167 | } 168 | 169 | return $parents; 170 | } 171 | 172 | /** 173 | * @param \voku\SimplePhpParser\Model\PHPClass $class 174 | * 175 | * @return array 176 | */ 177 | public function combineImplementedInterfaces($class): array 178 | { 179 | // init 180 | $interfaces = []; 181 | 182 | foreach ($class->interfaces as $interface) { 183 | $interfaces[] = $interface; 184 | 185 | $phpCodeInterfaces = $this->parserContainer->getInterface($interface); 186 | if ($phpCodeInterfaces !== null) { 187 | $interfaces[] = $phpCodeInterfaces->parentInterfaces; 188 | } 189 | } 190 | 191 | if ($class->parentClass === null) { 192 | return $interfaces; 193 | } 194 | 195 | $parentClass = $this->parserContainer->getClass($class->parentClass); 196 | if ($parentClass !== null) { 197 | $inherited = $this->combineImplementedInterfaces($parentClass); 198 | $interfaces = Utils::flattenArray($inherited, false); 199 | } 200 | 201 | return $interfaces; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPMethod.php: -------------------------------------------------------------------------------- 1 | prepareNode($node); 39 | 40 | $this->parentName = static::getFQN($node->getAttribute('parent')); 41 | 42 | $this->name = $node->name->name; 43 | 44 | $docComment = $node->getDocComment(); 45 | if ($docComment) { 46 | try { 47 | $phpDoc = Utils::createDocBlockInstance()->create($docComment->getText()); 48 | $this->summary = $phpDoc->getSummary(); 49 | $this->description = (string) $phpDoc->getDescription(); 50 | } catch (\Exception $e) { 51 | $tmpErrorMessage = \sprintf( 52 | '%s->%s:%s | %s', 53 | $this->parentName, 54 | $this->name, 55 | $this->line ?? '?', 56 | \print_r($e->getMessage(), true) 57 | ); 58 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 59 | } 60 | } 61 | 62 | if ($node->returnType) { 63 | if (!$this->returnType) { 64 | if (\method_exists($node->returnType, 'toString')) { 65 | $this->returnType = $node->returnType->toString(); 66 | } elseif (\property_exists($node->returnType, 'name') && $node->returnType->name) { 67 | $this->returnType = $node->returnType->name; 68 | } 69 | } 70 | 71 | if ($node->returnType instanceof \PhpParser\Node\NullableType) { 72 | if ($this->returnType && $this->returnType !== 'null' && \strpos($this->returnType, 'null|') !== 0) { 73 | $this->returnType = 'null|' . $this->returnType; 74 | } elseif (!$this->returnType) { 75 | $this->returnType = 'null|mixed'; 76 | } 77 | } 78 | } 79 | 80 | $this->collectTags($node); 81 | 82 | $docComment = $node->getDocComment(); 83 | if ($docComment) { 84 | $docCommentText = $docComment->getText(); 85 | 86 | if (\stripos($docCommentText, '@inheritdoc') !== false) { 87 | $this->is_inheritdoc = true; 88 | } 89 | 90 | $this->readPhpDoc($docComment); 91 | } 92 | 93 | $this->is_final = $node->isFinal(); 94 | 95 | $this->is_static = $node->isStatic(); 96 | 97 | if ($node->isPrivate()) { 98 | $this->access = 'private'; 99 | } elseif ($node->isProtected()) { 100 | $this->access = 'protected'; 101 | } else { 102 | $this->access = 'public'; 103 | } 104 | 105 | foreach ($node->getParams() as $parameter) { 106 | $parameterVar = $parameter->var; 107 | if ($parameterVar instanceof \PhpParser\Node\Expr\Error) { 108 | $this->parseError[] = \sprintf( 109 | '%s:%s | maybe at this position an expression is required', 110 | $this->line ?? '?', 111 | $this->pos ?? '' 112 | ); 113 | 114 | return $this; 115 | } 116 | 117 | $parameterNameTmp = $parameterVar->name; 118 | \assert(\is_string($parameterNameTmp)); 119 | 120 | if (isset($this->parameters[$parameterNameTmp])) { 121 | $this->parameters[$parameterNameTmp] = $this->parameters[$parameterNameTmp]->readObjectFromPhpNode($parameter, $node, $classStr); 122 | } else { 123 | $this->parameters[$parameterNameTmp] = (new PHPParameter($this->parserContainer))->readObjectFromPhpNode($parameter, $node, $classStr); 124 | } 125 | } 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * @param \ReflectionMethod $method 132 | * 133 | * @return $this 134 | */ 135 | public function readObjectFromReflection($method): PHPFunction 136 | { 137 | $this->name = $method->getName(); 138 | 139 | if (!$this->line) { 140 | $lineTmp = $method->getStartLine(); 141 | if ($lineTmp !== false) { 142 | $this->line = $lineTmp; 143 | } 144 | } 145 | 146 | $file = $method->getFileName(); 147 | if ($file) { 148 | $this->file = $file; 149 | } 150 | 151 | $this->is_static = $method->isStatic(); 152 | 153 | $this->is_final = $method->isFinal(); 154 | 155 | $returnType = $method->getReturnType(); 156 | if ($returnType !== null) { 157 | if (\method_exists($returnType, 'getName')) { 158 | $this->returnType = $returnType->getName(); 159 | } else { 160 | $this->returnType = $returnType . ''; 161 | } 162 | 163 | if ($returnType->allowsNull()) { 164 | if ($this->returnType && $this->returnType !== 'null' && \strpos($this->returnType, 'null|') !== 0) { 165 | $this->returnType = 'null|' . $this->returnType; 166 | } elseif (!$this->returnType) { 167 | $this->returnType = 'null|mixed'; 168 | } 169 | } 170 | } 171 | 172 | $docComment = $method->getDocComment(); 173 | if ($docComment) { 174 | if (\stripos($docComment, '@inheritdoc') !== false) { 175 | $this->is_inheritdoc = true; 176 | } 177 | 178 | $this->readPhpDoc($docComment); 179 | } 180 | 181 | if (!$this->returnTypeFromPhpDoc) { 182 | try { 183 | $phpDoc = DocFactoryProvider::getDocFactory()->create((string)$method->getDocComment()); 184 | $returnTypeTmp = $phpDoc->getTagsByName('return'); 185 | if ( 186 | \count($returnTypeTmp) === 1 187 | && 188 | $returnTypeTmp[0] instanceof \phpDocumentor\Reflection\DocBlock\Tags\Return_ 189 | ) { 190 | $this->returnTypeFromPhpDoc = Utils::parseDocTypeObject($returnTypeTmp[0]->getType()); 191 | } 192 | } catch (\Exception $e) { 193 | // ignore 194 | } 195 | } 196 | 197 | if ($method->isProtected()) { 198 | $access = 'protected'; 199 | } elseif ($method->isPrivate()) { 200 | $access = 'private'; 201 | } else { 202 | $access = 'public'; 203 | } 204 | $this->access = $access; 205 | 206 | foreach ($method->getParameters() as $parameter) { 207 | $param = (new PHPParameter($this->parserContainer))->readObjectFromReflection($parameter); 208 | $this->parameters[$param->name] = $param; 209 | } 210 | 211 | return $this; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Parsers/Helper/ParserContainer.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $constants = []; 21 | 22 | /** 23 | * @var \voku\SimplePhpParser\Model\PHPFunction[] 24 | * 25 | * @phpstan-var array 26 | */ 27 | private array $functions = []; 28 | 29 | /** 30 | * @var \voku\SimplePhpParser\Model\PHPClass[] 31 | * 32 | * @phpstan-var array 33 | */ 34 | private array $classes = []; 35 | 36 | /** 37 | * @var \voku\SimplePhpParser\Model\PHPTrait[] 38 | * 39 | * @phpstan-var array 40 | */ 41 | private array $traits = []; 42 | 43 | /** 44 | * @var \voku\SimplePhpParser\Model\PHPInterface[] 45 | * 46 | * @phpstan-var array 47 | */ 48 | private array $interfaces = []; 49 | 50 | /** 51 | * @var string[] 52 | */ 53 | private array $parse_errors = []; 54 | 55 | /** 56 | * @return \voku\SimplePhpParser\Model\PHPConst[] 57 | */ 58 | public function getConstants(): array 59 | { 60 | return $this->constants; 61 | } 62 | 63 | /** 64 | * @return string[] 65 | */ 66 | public function getParseErrors(): array 67 | { 68 | return $this->parse_errors; 69 | } 70 | 71 | public function addConstant(PHPConst $constant): void 72 | { 73 | $this->constants[$constant->name] = $constant; 74 | } 75 | 76 | /** 77 | * @return \voku\SimplePhpParser\Model\PHPFunction[] 78 | */ 79 | public function getFunctions(): array 80 | { 81 | return $this->functions; 82 | } 83 | 84 | /** 85 | * @param bool $skipDeprecatedFunctions 86 | * @param bool $skipFunctionsWithLeadingUnderscore 87 | * 88 | * @return array 89 | * 90 | * @psalm-return array, 107 | * returnTypes: array{ 108 | * type: null|string, 109 | * typeFromPhpDoc: null|string, 110 | * typeFromPhpDocExtended: null|string, 111 | * typeFromPhpDocSimple: null|string, 112 | * typeFromPhpDocMaybeWithComment: null|string 113 | * }, 114 | * paramsPhpDocRaw: array, 115 | * returnPhpDocRaw: null|string 116 | * }> 117 | */ 118 | public function getFunctionsInfo( 119 | bool $skipDeprecatedFunctions = false, 120 | bool $skipFunctionsWithLeadingUnderscore = false 121 | ): array { 122 | // init 123 | $allInfo = []; 124 | 125 | foreach ($this->functions as $function) { 126 | if ($skipDeprecatedFunctions && $function->hasDeprecatedTag) { 127 | continue; 128 | } 129 | 130 | if ($skipFunctionsWithLeadingUnderscore && \strpos($function->name, '_') === 0) { 131 | continue; 132 | } 133 | 134 | $paramsTypes = []; 135 | foreach ($function->parameters as $tagParam) { 136 | $paramsTypes[$tagParam->name]['type'] = $tagParam->type; 137 | $paramsTypes[$tagParam->name]['typeFromPhpDocMaybeWithComment'] = $tagParam->typeFromPhpDocMaybeWithComment; 138 | $paramsTypes[$tagParam->name]['typeFromPhpDoc'] = $tagParam->typeFromPhpDoc; 139 | $paramsTypes[$tagParam->name]['typeFromPhpDocSimple'] = $tagParam->typeFromPhpDocSimple; 140 | $paramsTypes[$tagParam->name]['typeFromPhpDocExtended'] = $tagParam->typeFromPhpDocExtended; 141 | $paramsTypes[$tagParam->name]['typeFromDefaultValue'] = $tagParam->typeFromDefaultValue; 142 | } 143 | 144 | $returnTypes = []; 145 | $returnTypes['type'] = $function->returnType; 146 | $returnTypes['typeFromPhpDocMaybeWithComment'] = $function->returnTypeFromPhpDocMaybeWithComment; 147 | $returnTypes['typeFromPhpDoc'] = $function->returnTypeFromPhpDoc; 148 | $returnTypes['typeFromPhpDocSimple'] = $function->returnTypeFromPhpDocSimple; 149 | $returnTypes['typeFromPhpDocExtended'] = $function->returnTypeFromPhpDocExtended; 150 | 151 | $paramsPhpDocRaw = []; 152 | foreach ($function->parameters as $tagParam) { 153 | $paramsPhpDocRaw[$tagParam->name] = $tagParam->phpDocRaw; 154 | } 155 | 156 | $infoTmp = []; 157 | $infoTmp['fullDescription'] = \trim($function->summary . "\n\n" . $function->description); 158 | $infoTmp['paramsTypes'] = $paramsTypes; 159 | $infoTmp['returnTypes'] = $returnTypes; 160 | $infoTmp['paramsPhpDocRaw'] = $paramsPhpDocRaw; 161 | $infoTmp['returnPhpDocRaw'] = $function->returnPhpDocRaw; 162 | $infoTmp['line'] = $function->line; 163 | $infoTmp['file'] = $function->file; 164 | $infoTmp['error'] = \implode("\n", $function->parseError); 165 | foreach ($function->parameters as $parameter) { 166 | $infoTmp['error'] .= ($infoTmp['error'] ? "\n" : '') . \implode("\n", $parameter->parseError); 167 | } 168 | $infoTmp['is_deprecated'] = $function->hasDeprecatedTag; 169 | $infoTmp['is_meta'] = $function->hasMetaTag; 170 | $infoTmp['is_internal'] = $function->hasInternalTag; 171 | $infoTmp['is_removed'] = $function->hasRemovedTag; 172 | 173 | $allInfo[$function->name] = $infoTmp; 174 | } 175 | 176 | \asort($allInfo); 177 | 178 | return $allInfo; 179 | } 180 | 181 | public function addFunction(PHPFunction $function): void 182 | { 183 | $this->functions[$function->name] = $function; 184 | } 185 | 186 | /** 187 | * @param string $name 188 | * 189 | * @return \voku\SimplePhpParser\Model\PHPClass|null 190 | */ 191 | public function getClass(string $name): ?PHPClass 192 | { 193 | return $this->classes[$name] ?? null; 194 | } 195 | 196 | /** 197 | * @return \voku\SimplePhpParser\Model\PHPClass[] 198 | */ 199 | public function getClasses(): array 200 | { 201 | return $this->classes; 202 | } 203 | 204 | /** 205 | * @return \voku\SimplePhpParser\Model\PHPClass[] 206 | */ 207 | public function &getClassesByReference(): array 208 | { 209 | return $this->classes; 210 | } 211 | 212 | /** 213 | * @param array $interfaces 214 | */ 215 | public function setInterfaces($interfaces): void 216 | { 217 | foreach ($interfaces as $name => $interface) { 218 | $this->interfaces[$name] = $interface; 219 | } 220 | } 221 | 222 | /** 223 | * @param array $constants 224 | */ 225 | public function setConstants($constants): void 226 | { 227 | foreach ($constants as $name => $constant) { 228 | $this->constants[$name] = $constant; 229 | } 230 | } 231 | 232 | /** 233 | * @param array $functions 234 | */ 235 | public function setFunctions($functions): void 236 | { 237 | foreach ($functions as $name => $function) { 238 | $this->functions[$name] = $function; 239 | } 240 | } 241 | 242 | /** 243 | * @param array $classes 244 | */ 245 | public function setClasses($classes): void 246 | { 247 | foreach ($classes as $className => $class) { 248 | $this->classes[$className] = $class; 249 | } 250 | } 251 | 252 | /** 253 | * @param array $traits 254 | */ 255 | public function setTraits($traits): void 256 | { 257 | foreach ($traits as $traitName => $trait) { 258 | $this->traits[$traitName] = $trait; 259 | } 260 | } 261 | 262 | public function addException(\Exception $exception): void 263 | { 264 | $this->parse_errors[] = $exception->getFile() . ':' . $exception->getLine() . ' | ' . $exception->getMessage(); 265 | } 266 | 267 | public function setParseError(ParserErrorHandler $error): void 268 | { 269 | foreach ($error->getErrors() as $errorInner) { 270 | $this->parse_errors[] = $errorInner->getFile() . ':' . $errorInner->getLine() . ' | ' . $errorInner->getMessage(); 271 | } 272 | } 273 | 274 | public function addClass(PHPClass $class): void 275 | { 276 | $this->classes[$class->name ?: \md5(\serialize($class))] = $class; 277 | } 278 | 279 | /** 280 | * @param string $name 281 | * 282 | * @return \voku\SimplePhpParser\Model\PHPTrait|null 283 | */ 284 | public function getTrait(string $name): ?PHPTrait 285 | { 286 | return $this->traits[$name] ?? null; 287 | } 288 | 289 | /** 290 | * @return \voku\SimplePhpParser\Model\PHPTrait[] 291 | */ 292 | public function getTraits(): array 293 | { 294 | return $this->traits; 295 | } 296 | 297 | public function addTrait(PHPTrait $trait): void 298 | { 299 | $this->traits[$trait->name ?: \md5(\serialize($trait))] = $trait; 300 | } 301 | 302 | /** 303 | * @param string $name 304 | * 305 | * @return \voku\SimplePhpParser\Model\PHPInterface|null 306 | */ 307 | public function getInterface(string $name): ?PHPInterface 308 | { 309 | return $this->interfaces[$name] ?? null; 310 | } 311 | 312 | /** 313 | * @return \voku\SimplePhpParser\Model\PHPInterface[] 314 | */ 315 | public function getInterfaces(): array 316 | { 317 | return $this->interfaces; 318 | } 319 | 320 | public function addInterface(PHPInterface $interface): void 321 | { 322 | $this->interfaces[$interface->name ?: \md5(\serialize($interface))] = $interface; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPProperty.php: -------------------------------------------------------------------------------- 1 | name = $this->getConstantFQN($node, $node->props[0]->name->name); 55 | 56 | $this->is_static = $node->isStatic(); 57 | 58 | if (method_exists($node, 'isReadonly')) { 59 | $this->is_readonly = $node->isReadonly(); 60 | } 61 | 62 | $this->prepareNode($node); 63 | 64 | $docComment = $node->getDocComment(); 65 | if ($docComment) { 66 | $docCommentText = $docComment->getText(); 67 | 68 | if (\stripos($docCommentText, '@inheritdoc') !== false) { 69 | $this->is_inheritdoc = true; 70 | } 71 | 72 | $this->readPhpDoc($docComment); 73 | } 74 | 75 | if ($node->type !== null) { 76 | if (!$this->type) { 77 | if (\method_exists($node->type, 'getParts')) { 78 | $parts = $node->type->getParts(); 79 | if (!empty($parts)) { 80 | $this->type = '\\' . \implode('\\', $parts); 81 | } 82 | } elseif (\property_exists($node->type, 'name') && $node->type->name) { 83 | $this->type = $node->type->name; 84 | } 85 | } 86 | 87 | if ($node->type instanceof \PhpParser\Node\NullableType) { 88 | if ($this->type && $this->type !== 'null' && \strpos($this->type, 'null|') !== 0) { 89 | $this->type = 'null|' . $this->type; 90 | } elseif (!$this->type) { 91 | $this->type = 'null|mixed'; 92 | } 93 | } 94 | } 95 | 96 | if ($node->props[0]->default !== null) { 97 | $defaultValue = Utils::getPhpParserValueFromNode($node->props[0]->default, $classStr); 98 | if ($defaultValue !== Utils::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER) { 99 | $this->defaultValue = $defaultValue; 100 | 101 | $this->typeFromDefaultValue = Utils::normalizePhpType(\gettype($this->defaultValue)); 102 | } 103 | } 104 | 105 | if ($node->isPrivate()) { 106 | $this->access = 'private'; 107 | } elseif ($node->isProtected()) { 108 | $this->access = 'protected'; 109 | } else { 110 | $this->access = 'public'; 111 | } 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * @param ReflectionProperty $property 118 | * 119 | * @return $this 120 | */ 121 | public function readObjectFromReflection($property): self 122 | { 123 | $this->name = $property->getName(); 124 | 125 | $file = $property->getDeclaringClass()->getFileName(); 126 | if ($file) { 127 | $this->file = $file; 128 | } 129 | 130 | $this->is_static = $property->isStatic(); 131 | 132 | if ($this->is_static) { 133 | try { 134 | if (\class_exists($property->getDeclaringClass()->getName(), true)) { 135 | $this->defaultValue = $property->getValue(); 136 | } 137 | } catch (\Exception $e) { 138 | // nothing 139 | } 140 | 141 | if ($this->defaultValue !== null) { 142 | $this->typeFromDefaultValue = Utils::normalizePhpType(\gettype($this->defaultValue)); 143 | } 144 | } 145 | 146 | if (method_exists($property, 'isReadOnly')) { 147 | $this->is_readonly = $property->isReadOnly(); 148 | } 149 | 150 | $docComment = $property->getDocComment(); 151 | if ($docComment) { 152 | if (\stripos($docComment, '@inheritdoc') !== false) { 153 | $this->is_inheritdoc = true; 154 | } 155 | 156 | $this->readPhpDoc($docComment); 157 | } 158 | 159 | if (\method_exists($property, 'getType')) { 160 | $type = $property->getType(); 161 | if ($type !== null) { 162 | if (\method_exists($type, 'getName')) { 163 | $this->type = Utils::normalizePhpType($type->getName(), true); 164 | } else { 165 | $this->type = Utils::normalizePhpType($type . '', true); 166 | } 167 | try { 168 | if ($this->type && \class_exists($this->type, true)) { 169 | $this->type = '\\' . \ltrim($this->type, '\\'); 170 | } 171 | } catch (\Exception $e) { 172 | // nothing 173 | } 174 | 175 | if ($type->allowsNull()) { 176 | if ($this->type && $this->type !== 'null' && \strpos($this->type, 'null|') !== 0) { 177 | $this->type = 'null|' . $this->type; 178 | } elseif (!$this->type) { 179 | $this->type = 'null|mixed'; 180 | } 181 | } 182 | } 183 | } 184 | 185 | if ($property->isProtected()) { 186 | $access = 'protected'; 187 | } elseif ($property->isPrivate()) { 188 | $access = 'private'; 189 | } else { 190 | $access = 'public'; 191 | } 192 | $this->access = $access; 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * @return string|null 199 | */ 200 | public function getType(): ?string 201 | { 202 | if ($this->typeFromPhpDocExtended) { 203 | return $this->typeFromPhpDocExtended; 204 | } 205 | 206 | if ($this->type) { 207 | return $this->type; 208 | } 209 | 210 | if ($this->typeFromPhpDocSimple) { 211 | return $this->typeFromPhpDocSimple; 212 | } 213 | 214 | return null; 215 | } 216 | 217 | /** 218 | * @param Doc|string $doc 219 | */ 220 | private function readPhpDoc($doc): void 221 | { 222 | if ($doc instanceof Doc) { 223 | $docComment = $doc->getText(); 224 | } else { 225 | $docComment = $doc; 226 | } 227 | if ($docComment === '') { 228 | return; 229 | } 230 | 231 | try { 232 | $phpDoc = Utils::createDocBlockInstance()->create($docComment); 233 | 234 | $parsedParamTags = $phpDoc->getTagsByName('var'); 235 | 236 | if (!empty($parsedParamTags)) { 237 | foreach ($parsedParamTags as $parsedParamTag) { 238 | $parsedParamTagParam = (string) $parsedParamTag; 239 | 240 | if ($parsedParamTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Var_) { 241 | $type = $parsedParamTag->getType(); 242 | 243 | $this->typeFromPhpDoc = Utils::normalizePhpType($type . ''); 244 | 245 | $typeFromPhpDocMaybeWithCommentTmp = \trim($parsedParamTagParam); 246 | if ( 247 | $typeFromPhpDocMaybeWithCommentTmp 248 | && 249 | \strpos($typeFromPhpDocMaybeWithCommentTmp, '$') !== 0 250 | ) { 251 | $this->typeFromPhpDocMaybeWithComment = $typeFromPhpDocMaybeWithCommentTmp; 252 | } 253 | 254 | $typeTmp = Utils::parseDocTypeObject($type); 255 | if ($typeTmp !== '') { 256 | $this->typeFromPhpDocSimple = $typeTmp; 257 | } 258 | } 259 | 260 | $this->phpDocRaw = $parsedParamTagParam; 261 | $this->typeFromPhpDocExtended = Utils::modernPhpdoc($parsedParamTagParam); 262 | } 263 | } 264 | 265 | $parsedParamTags = $phpDoc->getTagsByName('psalm-var') 266 | + $phpDoc->getTagsByName('phpstan-var'); 267 | 268 | if (!empty($parsedParamTags)) { 269 | foreach ($parsedParamTags as $parsedParamTag) { 270 | if (!$parsedParamTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Generic) { 271 | continue; 272 | } 273 | 274 | $spitedData = Utils::splitTypeAndVariable($parsedParamTag); 275 | $parsedParamTagStr = $spitedData['parsedParamTagStr']; 276 | 277 | $this->typeFromPhpDocExtended = Utils::modernPhpdoc($parsedParamTagStr); 278 | } 279 | } 280 | } catch (\Exception $e) { 281 | $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true); 282 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 283 | } 284 | 285 | try { 286 | $this->readPhpDocByTokens($docComment); 287 | } catch (\Exception $e) { 288 | $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true); 289 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 290 | } 291 | } 292 | 293 | /** 294 | * @throws \PHPStan\PhpDocParser\Parser\ParserException 295 | */ 296 | private function readPhpDocByTokens(string $docComment): void 297 | { 298 | $tokens = Utils::modernPhpdocTokens($docComment); 299 | 300 | $varContent = null; 301 | foreach ($tokens->getTokens() as $token) { 302 | $content = $token[0]; 303 | 304 | if ($content === '@var' || $content === '@psalm-var' || $content === '@phpstan-var') { 305 | // reset 306 | $varContent = ''; 307 | 308 | continue; 309 | } 310 | 311 | if ($varContent !== null) { 312 | $varContent .= $content; 313 | } 314 | } 315 | 316 | $varContent = $varContent ? \trim($varContent) : null; 317 | if ($varContent) { 318 | if (!$this->phpDocRaw) { 319 | $this->phpDocRaw = $varContent; 320 | } 321 | $this->typeFromPhpDocExtended = Utils::modernPhpdoc($varContent); 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPFunction.php: -------------------------------------------------------------------------------- 1 | prepareNode($node); 49 | 50 | $this->name = static::getFQN($node); 51 | 52 | /** @noinspection NotOptimalIfConditionsInspection */ 53 | if (\function_exists($this->name)) { 54 | $reflectionFunction = Utils::createFunctionReflectionInstance($this->name); 55 | $this->readObjectFromReflection($reflectionFunction); 56 | } 57 | 58 | if ($node->returnType) { 59 | if (!$this->returnType) { 60 | if (\method_exists($node->returnType, 'toString')) { 61 | $this->returnType = $node->returnType->toString(); 62 | } elseif (\property_exists($node->returnType, 'name') && $node->returnType->name) { 63 | $this->returnType = $node->returnType->name; 64 | } 65 | } 66 | 67 | if ($node->returnType instanceof \PhpParser\Node\NullableType) { 68 | if ($this->returnType && $this->returnType !== 'null' && \strpos($this->returnType, 'null|') !== 0) { 69 | $this->returnType = 'null|' . $this->returnType; 70 | } elseif (!$this->returnType) { 71 | $this->returnType = 'null|mixed'; 72 | } 73 | } 74 | } 75 | 76 | $docComment = $node->getDocComment(); 77 | if ($docComment) { 78 | try { 79 | $phpDoc = Utils::createDocBlockInstance()->create($docComment->getText()); 80 | $this->summary = $phpDoc->getSummary(); 81 | $this->description = (string) $phpDoc->getDescription(); 82 | } catch (\Exception $e) { 83 | $tmpErrorMessage = \sprintf( 84 | '%s:%s | %s', 85 | $this->name, 86 | $this->line ?? '?', 87 | \print_r($e->getMessage(), true) 88 | ); 89 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 90 | } 91 | } 92 | 93 | foreach ($node->getParams() as $parameter) { 94 | $parameterVar = $parameter->var; 95 | if ($parameterVar instanceof \PhpParser\Node\Expr\Error) { 96 | $this->parseError[] = \sprintf( 97 | '%s:%s | maybe at this position an expression is required', 98 | $this->line ?? '?', 99 | $this->pos ?? '' 100 | ); 101 | 102 | return $this; 103 | } 104 | 105 | $paramNameTmp = $parameterVar->name; 106 | \assert(\is_string($paramNameTmp)); 107 | 108 | if (isset($this->parameters[$paramNameTmp])) { 109 | $this->parameters[$paramNameTmp] = $this->parameters[$paramNameTmp]->readObjectFromPhpNode($parameter, $node); 110 | } else { 111 | $this->parameters[$paramNameTmp] = (new PHPParameter($this->parserContainer))->readObjectFromPhpNode($parameter, $node); 112 | } 113 | } 114 | 115 | $this->collectTags($node); 116 | 117 | $docComment = $node->getDocComment(); 118 | if ($docComment) { 119 | $this->readPhpDoc($docComment); 120 | } 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * @param ReflectionFunction $function 127 | * 128 | * @return $this 129 | */ 130 | public function readObjectFromReflection($function): self 131 | { 132 | $this->name = $function->getName(); 133 | 134 | if (!$this->line) { 135 | $lineTmp = $function->getStartLine(); 136 | if ($lineTmp !== false) { 137 | $this->line = $lineTmp; 138 | } 139 | } 140 | 141 | $file = $function->getFileName(); 142 | if ($file) { 143 | $this->file = $file; 144 | } 145 | 146 | $returnType = $function->getReturnType(); 147 | if ($returnType !== null) { 148 | if (\method_exists($returnType, 'getName')) { 149 | $this->returnType = $returnType->getName(); 150 | } else { 151 | $this->returnType = $returnType . ''; 152 | } 153 | 154 | if ($returnType->allowsNull()) { 155 | if ($this->returnType && $this->returnType !== 'null' && \strpos($this->returnType, 'null|') !== 0) { 156 | $this->returnType = 'null|' . $this->returnType; 157 | } elseif (!$this->returnType) { 158 | $this->returnType = 'null|mixed'; 159 | } 160 | } 161 | } 162 | 163 | $docComment = $function->getDocComment(); 164 | if ($docComment) { 165 | $this->readPhpDoc($docComment); 166 | } 167 | 168 | if (!$this->returnTypeFromPhpDoc) { 169 | try { 170 | $phpDoc = DocFactoryProvider::getDocFactory()->create((string)$function->getDocComment()); 171 | $returnTypeTmp = $phpDoc->getTagsByName('return'); 172 | if ( 173 | \count($returnTypeTmp) === 1 174 | && 175 | $returnTypeTmp[0] instanceof \phpDocumentor\Reflection\DocBlock\Tags\Return_ 176 | ) { 177 | $this->returnTypeFromPhpDoc = Utils::parseDocTypeObject($returnTypeTmp[0]->getType()); 178 | } 179 | } catch (\Exception $e) { 180 | // ignore 181 | } 182 | } 183 | 184 | foreach ($function->getParameters() as $parameter) { 185 | $param = (new PHPParameter($this->parserContainer))->readObjectFromReflection($parameter); 186 | $this->parameters[$param->name] = $param; 187 | } 188 | 189 | $docComment = $function->getDocComment(); 190 | if ($docComment) { 191 | $this->readPhpDoc($docComment); 192 | } 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * @return string|null 199 | */ 200 | public function getReturnType(): ?string 201 | { 202 | if ($this->returnTypeFromPhpDocExtended) { 203 | return $this->returnTypeFromPhpDocExtended; 204 | } 205 | 206 | if ($this->returnType) { 207 | return $this->returnType; 208 | } 209 | 210 | if ($this->returnTypeFromPhpDocSimple) { 211 | return $this->returnTypeFromPhpDocSimple; 212 | } 213 | 214 | return null; 215 | } 216 | 217 | /** 218 | * @param Doc|string $doc 219 | */ 220 | protected function readPhpDoc($doc): void 221 | { 222 | if ($doc instanceof Doc) { 223 | $docComment = $doc->getText(); 224 | } else { 225 | $docComment = $doc; 226 | } 227 | if ($docComment === '') { 228 | return; 229 | } 230 | 231 | try { 232 | $phpDoc = Utils::createDocBlockInstance()->create($docComment); 233 | 234 | $parsedReturnTag = $phpDoc->getTagsByName('return'); 235 | 236 | if (!empty($parsedReturnTag)) { 237 | /** @var Return_ $parsedReturnTagReturn */ 238 | $parsedReturnTagReturn = $parsedReturnTag[0]; 239 | 240 | if ($parsedReturnTagReturn instanceof Return_) { 241 | $this->returnTypeFromPhpDocMaybeWithComment = \trim((string) $parsedReturnTagReturn); 242 | 243 | $type = $parsedReturnTagReturn->getType(); 244 | 245 | $this->returnTypeFromPhpDoc = Utils::normalizePhpType(\ltrim((string) $type, '\\')); 246 | 247 | $typeTmp = Utils::parseDocTypeObject($type); 248 | if ($typeTmp !== '') { 249 | $this->returnTypeFromPhpDocSimple = $typeTmp; 250 | } 251 | } 252 | 253 | $this->returnPhpDocRaw = (string) $parsedReturnTagReturn; 254 | $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc((string) $parsedReturnTagReturn); 255 | } 256 | 257 | $parsedReturnTag = $phpDoc->getTagsByName('psalm-return') 258 | + $phpDoc->getTagsByName('phpstan-return'); 259 | 260 | if (!empty($parsedReturnTag) && $parsedReturnTag[0] instanceof Generic) { 261 | $parsedReturnTagReturn = (string) $parsedReturnTag[0]; 262 | 263 | $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc($parsedReturnTagReturn); 264 | } 265 | } catch (\Exception $e) { 266 | $tmpErrorMessage = \sprintf( 267 | '%s:%s | %s', 268 | $this->name, 269 | $this->line ?? '?', 270 | \print_r($e->getMessage(), true) 271 | ); 272 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 273 | } 274 | 275 | try { 276 | $this->readPhpDocByTokens($docComment); 277 | } catch (\Exception $e) { 278 | $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true); 279 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 280 | } 281 | } 282 | 283 | /** 284 | * @throws \PHPStan\PhpDocParser\Parser\ParserException 285 | */ 286 | private function readPhpDocByTokens(string $docComment): void 287 | { 288 | $tokens = Utils::modernPhpdocTokens($docComment); 289 | 290 | $returnContent = null; 291 | foreach ($tokens->getTokens() as $token) { 292 | $content = $token[0]; 293 | 294 | if ($content === '@return' || $content === '@psalm-return' || $content === '@phpstan-return') { 295 | // reset 296 | $returnContent = ''; 297 | 298 | continue; 299 | } 300 | 301 | // We can stop if we found the end. 302 | if ($content === '*/') { 303 | break; 304 | } 305 | 306 | if ($returnContent !== null) { 307 | $returnContent .= $content; 308 | } 309 | } 310 | 311 | $returnContent = $returnContent ? \trim($returnContent) : null; 312 | if ($returnContent) { 313 | if (!$this->returnPhpDocRaw) { 314 | $this->returnPhpDocRaw = $returnContent; 315 | } 316 | $this->returnTypeFromPhpDocExtended = Utils::modernPhpdoc($returnContent); 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPParameter.php: -------------------------------------------------------------------------------- 1 | var; 50 | if ($parameterVar instanceof \PhpParser\Node\Expr\Error) { 51 | $this->parseError[] = ($this->line ?? '?') . ':' . ($this->pos ?? '') . ' | may be at this position an expression is required'; 52 | 53 | $this->name = \md5(\uniqid('error', true)); 54 | 55 | return $this; 56 | } 57 | 58 | $this->name = \is_string($parameterVar->name) ? $parameterVar->name : ''; 59 | 60 | if ($node) { 61 | $this->prepareNode($node); 62 | 63 | $docComment = $node->getDocComment(); 64 | if ($docComment) { 65 | $docCommentText = $docComment->getText(); 66 | 67 | if (\stripos($docCommentText, '@inheritdoc') !== false) { 68 | $this->is_inheritdoc = true; 69 | } 70 | 71 | $this->readPhpDoc($docComment, $this->name); 72 | } 73 | } 74 | 75 | if ($parameter->type !== null) { 76 | if (!$this->type) { 77 | if (\method_exists($parameter->type, 'getParts')) { 78 | $parts = $parameter->type->getParts(); 79 | if (!empty($parts)) { 80 | $this->type = '\\' . \implode('\\', $parts); 81 | } 82 | } elseif (\property_exists($parameter->type, 'name')) { 83 | $this->type = $parameter->type->name; 84 | } 85 | } 86 | 87 | if ($parameter->type instanceof \PhpParser\Node\NullableType) { 88 | if ($this->type && $this->type !== 'null' && \strpos($this->type, 'null|') !== 0) { 89 | $this->type = 'null|' . $this->type; 90 | } elseif (!$this->type) { 91 | $this->type = 'null|mixed'; 92 | } 93 | } 94 | } 95 | 96 | if ($parameter->default) { 97 | $defaultValue = Utils::getPhpParserValueFromNode($parameter->default, $classStr, $this->parserContainer); 98 | if ($defaultValue !== Utils::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER) { 99 | $this->defaultValue = $defaultValue; 100 | 101 | $this->typeFromDefaultValue = Utils::normalizePhpType(\gettype($this->defaultValue)); 102 | } 103 | } 104 | 105 | $this->is_vararg = $parameter->variadic; 106 | 107 | $this->is_passed_by_ref = $parameter->byRef; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * @param ReflectionParameter $parameter 114 | * 115 | * @return $this 116 | */ 117 | public function readObjectFromReflection($parameter): self 118 | { 119 | $this->name = $parameter->getName(); 120 | 121 | if ($parameter->isDefaultValueAvailable()) { 122 | try { 123 | $this->defaultValue = $parameter->getDefaultValue(); 124 | } catch (\ReflectionException $e) { 125 | // nothing 126 | } 127 | if ($this->defaultValue !== null) { 128 | $this->typeFromDefaultValue = Utils::normalizePhpType(\gettype($this->defaultValue)); 129 | } 130 | } 131 | 132 | $method = $parameter->getDeclaringFunction(); 133 | 134 | $docComment = $method->getDocComment(); 135 | if ($docComment) { 136 | if (\stripos($docComment, '@inheritdoc') !== false) { 137 | $this->is_inheritdoc = true; 138 | } 139 | 140 | $this->readPhpDoc($docComment, $this->name); 141 | } 142 | 143 | try { 144 | $type = $parameter->getType(); 145 | } catch (\ReflectionException $e) { 146 | $type = null; 147 | } 148 | if ($type !== null) { 149 | if (\method_exists($type, 'getName')) { 150 | $this->type = Utils::normalizePhpType($type->getName(), true); 151 | } else { 152 | $this->type = Utils::normalizePhpType($type . '', true); 153 | } 154 | if ($this->type && \class_exists($this->type, true)) { 155 | $this->type = '\\' . \ltrim($this->type, '\\'); 156 | } 157 | 158 | try { 159 | $constNameTmp = $parameter->getDefaultValueConstantName(); 160 | if ($constNameTmp && \defined($constNameTmp)) { 161 | $defaultTmp = \constant($constNameTmp); 162 | if ($defaultTmp === null) { 163 | if ($this->type && $this->type !== 'null' && \strpos($this->type, 'null|') !== 0) { 164 | $this->type = 'null|' . $this->type; 165 | } elseif (!$this->type) { 166 | $this->type = 'null|mixed'; 167 | } 168 | } 169 | } 170 | } catch (\ReflectionException $e) { 171 | if ($type->allowsNull()) { 172 | if ($this->type && $this->type !== 'null' && \strpos($this->type, 'null|') !== 0) { 173 | $this->type = 'null|' . $this->type; 174 | } elseif (!$this->type) { 175 | $this->type = 'null|mixed'; 176 | } 177 | } 178 | } 179 | } 180 | 181 | $this->is_vararg = $parameter->isVariadic(); 182 | 183 | $this->is_passed_by_ref = $parameter->isPassedByReference(); 184 | 185 | return $this; 186 | } 187 | 188 | /** 189 | * @return string|null 190 | */ 191 | public function getType(): ?string 192 | { 193 | if ($this->typeFromPhpDocExtended) { 194 | return $this->typeFromPhpDocExtended; 195 | } 196 | 197 | if ($this->type) { 198 | return $this->type; 199 | } 200 | 201 | if ($this->typeFromPhpDocSimple) { 202 | return $this->typeFromPhpDocSimple; 203 | } 204 | 205 | return null; 206 | } 207 | 208 | /** 209 | * @param Doc|string $doc 210 | */ 211 | private function readPhpDoc($doc, string $parameterName): void 212 | { 213 | if ($doc instanceof Doc) { 214 | $docComment = $doc->getText(); 215 | } else { 216 | $docComment = $doc; 217 | } 218 | if ($docComment === '') { 219 | return; 220 | } 221 | 222 | try { 223 | $phpDoc = Utils::createDocBlockInstance()->create($docComment); 224 | 225 | $parsedParamTags = $phpDoc->getTagsByName('param'); 226 | 227 | if (!empty($parsedParamTags)) { 228 | foreach ($parsedParamTags as $parsedParamTag) { 229 | if ($parsedParamTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Param) { 230 | // check only the current "param"-tag 231 | if (\strtoupper($parameterName) !== \strtoupper((string) $parsedParamTag->getVariableName())) { 232 | continue; 233 | } 234 | 235 | $type = $parsedParamTag->getType(); 236 | 237 | $this->typeFromPhpDoc = Utils::normalizePhpType($type . ''); 238 | 239 | $typeFromPhpDocMaybeWithCommentTmp = \trim((string) $parsedParamTag); 240 | if ( 241 | $typeFromPhpDocMaybeWithCommentTmp 242 | && 243 | \strpos($typeFromPhpDocMaybeWithCommentTmp, '$') !== 0 244 | ) { 245 | $this->typeFromPhpDocMaybeWithComment = $typeFromPhpDocMaybeWithCommentTmp; 246 | } 247 | 248 | $typeTmp = Utils::parseDocTypeObject($type); 249 | if ($typeTmp !== '') { 250 | $this->typeFromPhpDocSimple = $typeTmp; 251 | } 252 | } 253 | 254 | $parsedParamTagParam = (string) $parsedParamTag; 255 | $spitedData = Utils::splitTypeAndVariable($parsedParamTag); 256 | $variableName = $spitedData['variableName']; 257 | 258 | // check only the current "param"-tag 259 | if ($variableName && \strtoupper($parameterName) === \strtoupper($variableName)) { 260 | $this->phpDocRaw = $parsedParamTagParam; 261 | $this->typeFromPhpDocExtended = Utils::modernPhpdoc($parsedParamTagParam); 262 | } 263 | 264 | break; 265 | } 266 | } 267 | 268 | $parsedParamTags = $phpDoc->getTagsByName('psalm-param') 269 | + $phpDoc->getTagsByName('phpstan-param'); 270 | 271 | if (!empty($parsedParamTags)) { 272 | foreach ($parsedParamTags as $parsedParamTag) { 273 | if (!$parsedParamTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Generic) { 274 | continue; 275 | } 276 | 277 | $spitedData = Utils::splitTypeAndVariable($parsedParamTag); 278 | $parsedParamTagStr = $spitedData['parsedParamTagStr']; 279 | $variableName = $spitedData['variableName']; 280 | 281 | // check only the current "param"-tag 282 | if (!$variableName || \strtoupper($parameterName) !== \strtoupper($variableName)) { 283 | continue; 284 | } 285 | 286 | $this->typeFromPhpDocExtended = Utils::modernPhpdoc($parsedParamTagStr); 287 | } 288 | } 289 | } catch (\Exception $e) { 290 | $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true); 291 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 292 | } 293 | 294 | try { 295 | $this->readPhpDocByTokens($docComment, $parameterName); 296 | } catch (\Exception $e) { 297 | $tmpErrorMessage = $this->name . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true); 298 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 299 | } 300 | } 301 | 302 | /** 303 | * @throws \PHPStan\PhpDocParser\Parser\ParserException 304 | */ 305 | private function readPhpDocByTokens(string $docComment, string $parameterName): void 306 | { 307 | $tokens = Utils::modernPhpdocTokens($docComment); 308 | 309 | $paramContent = null; 310 | foreach ($tokens->getTokens() as $token) { 311 | $content = $token[0]; 312 | 313 | if ($content === '@param' || $content === '@psalm-param' || $content === '@phpstan-param') { 314 | // reset 315 | $paramContent = ''; 316 | 317 | continue; 318 | } 319 | 320 | // We can stop if we found the param variable e.g. `@param array{foo:int} $param`. 321 | if ($content === '$' . $parameterName) { 322 | break; 323 | } 324 | 325 | if ($paramContent !== null) { 326 | $paramContent .= $content; 327 | } 328 | } 329 | 330 | $paramContent = $paramContent ? \trim($paramContent) : null; 331 | if ($paramContent) { 332 | if (!$this->phpDocRaw) { 333 | $this->phpDocRaw = $paramContent . ' ' . '$' . $parameterName; 334 | } 335 | $this->typeFromPhpDocExtended = Utils::modernPhpdoc($paramContent); 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPTrait.php: -------------------------------------------------------------------------------- 1 | prepareNode($node); 28 | 29 | $this->name = static::getFQN($node); 30 | 31 | if (\trait_exists($this->name, true)) { 32 | $reflectionClass = Utils::createClassReflectionInstance($this->name); 33 | $this->readObjectFromReflection($reflectionClass); 34 | } 35 | 36 | $this->collectTags($node); 37 | 38 | $docComment = $node->getDocComment(); 39 | if ($docComment) { 40 | $this->readPhpDocProperties($docComment); 41 | } 42 | 43 | foreach ($node->getProperties() as $property) { 44 | $propertyNameTmp = $this->getConstantFQN($property, $property->props[0]->name->name); 45 | 46 | if (isset($this->properties[$propertyNameTmp])) { 47 | $this->properties[$propertyNameTmp] = $this->properties[$propertyNameTmp]->readObjectFromPhpNode($property, $this->name); 48 | } else { 49 | $this->properties[$propertyNameTmp] = (new PHPProperty($this->parserContainer))->readObjectFromPhpNode($property, $this->name); 50 | } 51 | } 52 | 53 | foreach ($node->getMethods() as $method) { 54 | $methodNameTmp = $method->name->name; 55 | 56 | if (isset($this->methods[$methodNameTmp])) { 57 | $this->methods[$methodNameTmp] = $this->methods[$methodNameTmp]->readObjectFromPhpNode($method, $this->name); 58 | } else { 59 | $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromPhpNode($method, $this->name); 60 | } 61 | 62 | if (!$this->methods[$methodNameTmp]->file) { 63 | $this->methods[$methodNameTmp]->file = $this->file; 64 | } 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * @param ReflectionClass $clazz 72 | * 73 | * @return $this 74 | */ 75 | public function readObjectFromReflection($clazz): self 76 | { 77 | if (!$clazz->isTrait()) { 78 | return $this; 79 | } 80 | 81 | $this->name = $clazz->getName(); 82 | 83 | if (!$this->line) { 84 | $lineTmp = $clazz->getStartLine(); 85 | if ($lineTmp !== false) { 86 | $this->line = $lineTmp; 87 | } 88 | } 89 | 90 | $file = $clazz->getFileName(); 91 | if ($file) { 92 | $this->file = $file; 93 | } 94 | 95 | $this->is_final = $clazz->isFinal(); 96 | 97 | $this->is_abstract = $clazz->isAbstract(); 98 | 99 | $this->is_anonymous = $clazz->isAnonymous(); 100 | 101 | $this->is_cloneable = $clazz->isCloneable(); 102 | 103 | $this->is_instantiable = $clazz->isInstantiable(); 104 | 105 | $this->is_iterable = $clazz->isIterable(); 106 | 107 | foreach ($clazz->getProperties() as $property) { 108 | $propertyPhp = (new PHPProperty($this->parserContainer))->readObjectFromReflection($property); 109 | $this->properties[$propertyPhp->name] = $propertyPhp; 110 | } 111 | 112 | foreach ($clazz->getMethods() as $method) { 113 | $methodNameTmp = $method->getName(); 114 | 115 | $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromReflection($method); 116 | 117 | if (!$this->methods[$methodNameTmp]->file) { 118 | $this->methods[$methodNameTmp]->file = $this->file; 119 | } 120 | } 121 | 122 | foreach ($clazz->getReflectionConstants() as $constant) { 123 | $constantNameTmp = $constant->getName(); 124 | 125 | $this->constants[$constantNameTmp] = (new PHPConst($this->parserContainer))->readObjectFromReflection($constant); 126 | 127 | if (!$this->constants[$constantNameTmp]->file) { 128 | $this->constants[$constantNameTmp]->file = $this->file; 129 | } 130 | } 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * @param string[] $access 137 | * @param bool $skipMethodsWithLeadingUnderscore 138 | * 139 | * @return array 140 | * 141 | * @psalm-return array 142 | */ 143 | public function getPropertiesInfo( 144 | array $access = ['public', 'protected', 'private'], 145 | bool $skipMethodsWithLeadingUnderscore = false 146 | ): array { 147 | // init 148 | $allInfo = []; 149 | 150 | foreach ($this->properties as $property) { 151 | if (!\in_array($property->access, $access, true)) { 152 | continue; 153 | } 154 | 155 | if ($skipMethodsWithLeadingUnderscore && \strpos($property->name, '_') === 0) { 156 | continue; 157 | } 158 | 159 | $types = []; 160 | $types['type'] = $property->type; 161 | $types['typeFromPhpDocMaybeWithComment'] = $property->typeFromPhpDocMaybeWithComment; 162 | $types['typeFromPhpDoc'] = $property->typeFromPhpDoc; 163 | $types['typeFromPhpDocSimple'] = $property->typeFromPhpDocSimple; 164 | $types['typeFromPhpDocExtended'] = $property->typeFromPhpDocExtended; 165 | $types['typeFromDefaultValue'] = $property->typeFromDefaultValue; 166 | 167 | $allInfo[$property->name] = $types; 168 | } 169 | 170 | return $allInfo; 171 | } 172 | 173 | /** 174 | * @param string[] $access 175 | * @param bool $skipDeprecatedMethods 176 | * @param bool $skipMethodsWithLeadingUnderscore 177 | * 178 | * @return array 179 | * 180 | * @psalm-return array, 200 | * returnTypes: array{ 201 | * type: null|string, 202 | * typeFromPhpDoc: null|string, 203 | * typeFromPhpDocExtended: null|string, 204 | * typeFromPhpDocSimple: null|string, 205 | * typeFromPhpDocMaybeWithComment: null|string 206 | * }, 207 | * paramsPhpDocRaw: array, 208 | * returnPhpDocRaw: null|string 209 | * }> 210 | */ 211 | public function getMethodsInfo( 212 | array $access = ['public', 'protected', 'private'], 213 | bool $skipDeprecatedMethods = false, 214 | bool $skipMethodsWithLeadingUnderscore = false 215 | ): array { 216 | // init 217 | $allInfo = []; 218 | 219 | foreach ($this->methods as $method) { 220 | if (!\in_array($method->access, $access, true)) { 221 | continue; 222 | } 223 | 224 | if ($skipDeprecatedMethods && $method->hasDeprecatedTag) { 225 | continue; 226 | } 227 | 228 | if ($skipMethodsWithLeadingUnderscore && \strpos($method->name, '_') === 0) { 229 | continue; 230 | } 231 | 232 | $paramsTypes = []; 233 | foreach ($method->parameters as $tagParam) { 234 | $paramsTypes[$tagParam->name]['type'] = $tagParam->type; 235 | $paramsTypes[$tagParam->name]['typeFromPhpDocMaybeWithComment'] = $tagParam->typeFromPhpDocMaybeWithComment; 236 | $paramsTypes[$tagParam->name]['typeFromPhpDoc'] = $tagParam->typeFromPhpDoc; 237 | $paramsTypes[$tagParam->name]['typeFromPhpDocSimple'] = $tagParam->typeFromPhpDocSimple; 238 | $paramsTypes[$tagParam->name]['typeFromPhpDocExtended'] = $tagParam->typeFromPhpDocExtended; 239 | $paramsTypes[$tagParam->name]['typeFromDefaultValue'] = $tagParam->typeFromDefaultValue; 240 | } 241 | 242 | $returnTypes = []; 243 | $returnTypes['type'] = $method->returnType; 244 | $returnTypes['typeFromPhpDocMaybeWithComment'] = $method->returnTypeFromPhpDocMaybeWithComment; 245 | $returnTypes['typeFromPhpDoc'] = $method->returnTypeFromPhpDoc; 246 | $returnTypes['typeFromPhpDocSimple'] = $method->returnTypeFromPhpDocSimple; 247 | $returnTypes['typeFromPhpDocExtended'] = $method->returnTypeFromPhpDocExtended; 248 | 249 | $paramsPhpDocRaw = []; 250 | foreach ($method->parameters as $tagParam) { 251 | $paramsPhpDocRaw[$tagParam->name] = $tagParam->phpDocRaw; 252 | } 253 | 254 | $infoTmp = []; 255 | $infoTmp['fullDescription'] = \trim($method->summary . "\n\n" . $method->description); 256 | $infoTmp['paramsTypes'] = $paramsTypes; 257 | $infoTmp['returnTypes'] = $returnTypes; 258 | $infoTmp['paramsPhpDocRaw'] = $paramsPhpDocRaw; 259 | $infoTmp['returnPhpDocRaw'] = $method->returnPhpDocRaw; 260 | $infoTmp['line'] = $method->line ?? $this->line; 261 | $infoTmp['file'] = $method->file ?? $this->file; 262 | $infoTmp['error'] = \implode("\n", $method->parseError); 263 | foreach ($method->parameters as $parameter) { 264 | $infoTmp['error'] .= ($infoTmp['error'] ? "\n" : '') . \implode("\n", $parameter->parseError); 265 | } 266 | $infoTmp['is_deprecated'] = $method->hasDeprecatedTag; 267 | $infoTmp['is_static'] = $method->is_static; 268 | $infoTmp['is_meta'] = $method->hasMetaTag; 269 | $infoTmp['is_internal'] = $method->hasInternalTag; 270 | $infoTmp['is_removed'] = $method->hasRemovedTag; 271 | 272 | $allInfo[$method->name] = $infoTmp; 273 | } 274 | 275 | \asort($allInfo); 276 | 277 | return $allInfo; 278 | } 279 | 280 | /** 281 | * @param Doc|string $doc 282 | */ 283 | private function readPhpDocProperties($doc): void 284 | { 285 | if ($doc instanceof Doc) { 286 | $docComment = $doc->getText(); 287 | } else { 288 | $docComment = $doc; 289 | } 290 | if ($docComment === '') { 291 | return; 292 | } 293 | 294 | try { 295 | $phpDoc = Utils::createDocBlockInstance()->create($docComment); 296 | 297 | $parsedPropertyTags = $phpDoc->getTagsByName('property') 298 | + $phpDoc->getTagsByName('property-read') 299 | + $phpDoc->getTagsByName('property-write'); 300 | 301 | if (!empty($parsedPropertyTags)) { 302 | foreach ($parsedPropertyTags as $parsedPropertyTag) { 303 | if ( 304 | $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyRead 305 | || 306 | $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyWrite 307 | || 308 | $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Property 309 | ) { 310 | $propertyPhp = new PHPProperty($this->parserContainer); 311 | 312 | $nameTmp = $parsedPropertyTag->getVariableName(); 313 | if (!$nameTmp) { 314 | continue; 315 | } 316 | 317 | $propertyPhp->name = $nameTmp; 318 | 319 | $propertyPhp->access = 'public'; 320 | 321 | $type = $parsedPropertyTag->getType(); 322 | 323 | $propertyPhp->typeFromPhpDoc = Utils::normalizePhpType($type . ''); 324 | 325 | $typeFromPhpDocMaybeWithCommentTmp = \trim((string) $parsedPropertyTag); 326 | if ( 327 | $typeFromPhpDocMaybeWithCommentTmp 328 | && 329 | \strpos($typeFromPhpDocMaybeWithCommentTmp, '$') !== 0 330 | ) { 331 | $propertyPhp->typeFromPhpDocMaybeWithComment = $typeFromPhpDocMaybeWithCommentTmp; 332 | } 333 | 334 | $typeTmp = Utils::parseDocTypeObject($type); 335 | if ($typeTmp !== '') { 336 | $propertyPhp->typeFromPhpDocSimple = $typeTmp; 337 | } 338 | 339 | if ($propertyPhp->typeFromPhpDoc) { 340 | $propertyPhp->typeFromPhpDocExtended = Utils::modernPhpdoc($propertyPhp->typeFromPhpDoc); 341 | } 342 | 343 | $this->properties[$propertyPhp->name] = $propertyPhp; 344 | } 345 | } 346 | } 347 | } catch (\Exception $e) { 348 | $tmpErrorMessage = ($this->name ?: '?') . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true); 349 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Model/PHPClass.php: -------------------------------------------------------------------------------- 1 | prepareNode($node); 40 | 41 | $this->name = static::getFQN($node); 42 | 43 | $this->is_final = $node->isFinal(); 44 | 45 | $this->is_abstract = $node->isAbstract(); 46 | 47 | if (method_exists($node, 'isReadOnly')) { 48 | $this->is_readonly = $node->isReadOnly(); 49 | } 50 | 51 | $this->is_anonymous = $node->isAnonymous(); 52 | 53 | $classExists = false; 54 | try { 55 | if (\class_exists($this->name, true)) { 56 | $classExists = true; 57 | } 58 | } catch (\Exception $e) { 59 | // nothing 60 | } 61 | if ($classExists) { 62 | $reflectionClass = Utils::createClassReflectionInstance($this->name); 63 | $this->readObjectFromReflection($reflectionClass); 64 | } 65 | 66 | $this->collectTags($node); 67 | 68 | if (!empty($node->extends)) { 69 | $classExtended = implode('\\', $node->extends->getParts()); 70 | /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */ 71 | /** @var class-string $classExtended */ 72 | $classExtended = $classExtended; 73 | $this->parentClass = $classExtended; 74 | } 75 | 76 | $docComment = $node->getDocComment(); 77 | if ($docComment) { 78 | $this->readPhpDocProperties($docComment->getText()); 79 | } 80 | 81 | foreach ($node->getProperties() as $property) { 82 | $propertyNameTmp = $this->getConstantFQN($property, $property->props[0]->name->name); 83 | 84 | if (isset($this->properties[$propertyNameTmp])) { 85 | $this->properties[$propertyNameTmp] = $this->properties[$propertyNameTmp]->readObjectFromPhpNode($property, $this->name); 86 | } else { 87 | $this->properties[$propertyNameTmp] = (new PHPProperty($this->parserContainer))->readObjectFromPhpNode($property, $this->name); 88 | } 89 | 90 | if ($this->is_readonly) { 91 | $this->properties[$propertyNameTmp]->is_readonly = true; 92 | } 93 | } 94 | 95 | foreach ($node->getMethods() as $method) { 96 | $methodNameTmp = $method->name->name; 97 | 98 | if (isset($this->methods[$methodNameTmp])) { 99 | $this->methods[$methodNameTmp] = $this->methods[$methodNameTmp]->readObjectFromPhpNode($method, $this->name); 100 | } else { 101 | $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromPhpNode($method, $this->name); 102 | } 103 | 104 | if (!$this->methods[$methodNameTmp]->file) { 105 | $this->methods[$methodNameTmp]->file = $this->file; 106 | } 107 | } 108 | 109 | if (!empty($node->implements)) { 110 | foreach ($node->implements as $interfaceObject) { 111 | $interfaceFQN = implode('\\', $interfaceObject->getParts()); 112 | /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */ 113 | /** @var class-string $interfaceFQN */ 114 | $interfaceFQN = $interfaceFQN; 115 | $this->interfaces[$interfaceFQN] = $interfaceFQN; 116 | } 117 | } 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * @param ReflectionClass $clazz 124 | * 125 | * @return $this 126 | */ 127 | public function readObjectFromReflection($clazz): self 128 | { 129 | $this->name = $clazz->getName(); 130 | 131 | if (!$this->line) { 132 | $lineTmp = $clazz->getStartLine(); 133 | if ($lineTmp !== false) { 134 | $this->line = $lineTmp; 135 | } 136 | } 137 | 138 | $file = $clazz->getFileName(); 139 | if ($file) { 140 | $this->file = $file; 141 | } 142 | 143 | $this->is_final = $clazz->isFinal(); 144 | 145 | $this->is_abstract = $clazz->isAbstract(); 146 | 147 | if (method_exists($clazz, 'isReadOnly')) { 148 | $this->is_readonly = $clazz->isReadOnly(); 149 | } 150 | 151 | $this->is_anonymous = $clazz->isAnonymous(); 152 | 153 | $this->is_cloneable = $clazz->isCloneable(); 154 | 155 | $this->is_instantiable = $clazz->isInstantiable(); 156 | 157 | $this->is_iterable = $clazz->isIterable(); 158 | 159 | $parent = $clazz->getParentClass(); 160 | if ($parent) { 161 | $this->parentClass = $parent->getName(); 162 | 163 | $classExists = false; 164 | try { 165 | if ( 166 | !$this->parserContainer->getClass($this->parentClass) 167 | && 168 | \class_exists($this->parentClass, true) 169 | ) { 170 | $classExists = true; 171 | } 172 | } catch (\Exception $e) { 173 | // nothing 174 | } 175 | if ($classExists) { 176 | $reflectionClass = Utils::createClassReflectionInstance($this->parentClass); 177 | $class = (new self($this->parserContainer))->readObjectFromReflection($reflectionClass); 178 | $this->parserContainer->addClass($class); 179 | } 180 | } 181 | 182 | foreach ($clazz->getProperties() as $property) { 183 | $propertyPhp = (new PHPProperty($this->parserContainer))->readObjectFromReflection($property); 184 | $this->properties[$propertyPhp->name] = $propertyPhp; 185 | 186 | if ($this->is_readonly) { 187 | $this->properties[$propertyPhp->name]->is_readonly = true; 188 | } 189 | } 190 | 191 | foreach ($clazz->getInterfaceNames() as $interfaceName) { 192 | /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */ 193 | /** @var class-string $interfaceName */ 194 | $interfaceName = $interfaceName; 195 | $this->interfaces[$interfaceName] = $interfaceName; 196 | } 197 | 198 | foreach ($clazz->getMethods() as $method) { 199 | $methodNameTmp = $method->getName(); 200 | 201 | $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromReflection($method); 202 | 203 | if (!$this->methods[$methodNameTmp]->file) { 204 | $this->methods[$methodNameTmp]->file = $this->file; 205 | } 206 | } 207 | 208 | foreach ($clazz->getReflectionConstants() as $constant) { 209 | $constantNameTmp = $constant->getName(); 210 | 211 | $this->constants[$constantNameTmp] = (new PHPConst($this->parserContainer))->readObjectFromReflection($constant); 212 | 213 | if (!$this->constants[$constantNameTmp]->file) { 214 | $this->constants[$constantNameTmp]->file = $this->file; 215 | } 216 | } 217 | 218 | return $this; 219 | } 220 | 221 | /** 222 | * @param string[] $access 223 | * @param bool $skipMethodsWithLeadingUnderscore 224 | * 225 | * @return array 226 | * 227 | * @psalm-return array 235 | */ 236 | public function getPropertiesInfo( 237 | array $access = ['public', 'protected', 'private'], 238 | bool $skipMethodsWithLeadingUnderscore = false 239 | ): array { 240 | // init 241 | $allInfo = []; 242 | 243 | foreach ($this->properties as $property) { 244 | if (!\in_array($property->access, $access, true)) { 245 | continue; 246 | } 247 | 248 | if ($skipMethodsWithLeadingUnderscore && \strpos($property->name, '_') === 0) { 249 | continue; 250 | } 251 | 252 | $types = []; 253 | $types['type'] = $property->type; 254 | $types['typeFromPhpDocMaybeWithComment'] = $property->typeFromPhpDocMaybeWithComment; 255 | $types['typeFromPhpDoc'] = $property->typeFromPhpDoc; 256 | $types['typeFromPhpDocSimple'] = $property->typeFromPhpDocSimple; 257 | $types['typeFromPhpDocExtended'] = $property->typeFromPhpDocExtended; 258 | $types['typeFromDefaultValue'] = $property->typeFromDefaultValue; 259 | 260 | $allInfo[$property->name] = $types; 261 | } 262 | 263 | return $allInfo; 264 | } 265 | 266 | /** 267 | * @param string[] $access 268 | * @param bool $skipDeprecatedMethods 269 | * @param bool $skipMethodsWithLeadingUnderscore 270 | * 271 | * @return array 272 | * 273 | * @psalm-return array, 293 | * returnTypes: array{ 294 | * type: null|string, 295 | * typeFromPhpDoc: null|string, 296 | * typeFromPhpDocExtended: null|string, 297 | * typeFromPhpDocSimple: null|string, 298 | * typeFromPhpDocMaybeWithComment: null|string 299 | * }, 300 | * paramsPhpDocRaw: array, 301 | * returnPhpDocRaw: null|string 302 | * }> 303 | */ 304 | public function getMethodsInfo( 305 | array $access = ['public', 'protected', 'private'], 306 | bool $skipDeprecatedMethods = false, 307 | bool $skipMethodsWithLeadingUnderscore = false 308 | ): array { 309 | // init 310 | $allInfo = []; 311 | 312 | foreach ($this->methods as $method) { 313 | if (!\in_array($method->access, $access, true)) { 314 | continue; 315 | } 316 | 317 | if ($skipDeprecatedMethods && $method->hasDeprecatedTag) { 318 | continue; 319 | } 320 | 321 | if ($skipMethodsWithLeadingUnderscore && \strpos($method->name, '_') === 0) { 322 | continue; 323 | } 324 | 325 | $paramsTypes = []; 326 | foreach ($method->parameters as $tagParam) { 327 | $paramsTypes[$tagParam->name]['type'] = $tagParam->type; 328 | $paramsTypes[$tagParam->name]['typeFromPhpDocMaybeWithComment'] = $tagParam->typeFromPhpDocMaybeWithComment; 329 | $paramsTypes[$tagParam->name]['typeFromPhpDoc'] = $tagParam->typeFromPhpDoc; 330 | $paramsTypes[$tagParam->name]['typeFromPhpDocSimple'] = $tagParam->typeFromPhpDocSimple; 331 | $paramsTypes[$tagParam->name]['typeFromPhpDocExtended'] = $tagParam->typeFromPhpDocExtended; 332 | $paramsTypes[$tagParam->name]['typeFromDefaultValue'] = $tagParam->typeFromDefaultValue; 333 | } 334 | 335 | $returnTypes = []; 336 | $returnTypes['type'] = $method->returnType; 337 | $returnTypes['typeFromPhpDocMaybeWithComment'] = $method->returnTypeFromPhpDocMaybeWithComment; 338 | $returnTypes['typeFromPhpDoc'] = $method->returnTypeFromPhpDoc; 339 | $returnTypes['typeFromPhpDocSimple'] = $method->returnTypeFromPhpDocSimple; 340 | $returnTypes['typeFromPhpDocExtended'] = $method->returnTypeFromPhpDocExtended; 341 | 342 | $paramsPhpDocRaw = []; 343 | foreach ($method->parameters as $tagParam) { 344 | $paramsPhpDocRaw[$tagParam->name] = $tagParam->phpDocRaw; 345 | } 346 | 347 | $infoTmp = []; 348 | $infoTmp['fullDescription'] = \trim($method->summary . "\n\n" . $method->description); 349 | $infoTmp['paramsTypes'] = $paramsTypes; 350 | $infoTmp['returnTypes'] = $returnTypes; 351 | $infoTmp['paramsPhpDocRaw'] = $paramsPhpDocRaw; 352 | $infoTmp['returnPhpDocRaw'] = $method->returnPhpDocRaw; 353 | $infoTmp['line'] = $method->line ?? $this->line; 354 | $infoTmp['file'] = $method->file ?? $this->file; 355 | $infoTmp['error'] = \implode("\n", $method->parseError); 356 | foreach ($method->parameters as $parameter) { 357 | $infoTmp['error'] .= ($infoTmp['error'] ? "\n" : '') . \implode("\n", $parameter->parseError); 358 | } 359 | $infoTmp['is_deprecated'] = $method->hasDeprecatedTag; 360 | $infoTmp['is_static'] = $method->is_static; 361 | $infoTmp['is_meta'] = $method->hasMetaTag; 362 | $infoTmp['is_internal'] = $method->hasInternalTag; 363 | $infoTmp['is_removed'] = $method->hasRemovedTag; 364 | 365 | $allInfo[$method->name] = $infoTmp; 366 | } 367 | 368 | \asort($allInfo); 369 | 370 | return $allInfo; 371 | } 372 | 373 | /** 374 | * @param Doc|string $doc 375 | */ 376 | private function readPhpDocProperties($doc): void 377 | { 378 | if ($doc instanceof Doc) { 379 | $docComment = $doc->getText(); 380 | } else { 381 | $docComment = $doc; 382 | } 383 | if ($docComment === '') { 384 | return; 385 | } 386 | 387 | try { 388 | $phpDoc = Utils::createDocBlockInstance()->create($docComment); 389 | 390 | $parsedPropertyTags = $phpDoc->getTagsByName('property') 391 | + $phpDoc->getTagsByName('property-read') 392 | + $phpDoc->getTagsByName('property-write'); 393 | 394 | if (!empty($parsedPropertyTags)) { 395 | foreach ($parsedPropertyTags as $parsedPropertyTag) { 396 | if ( 397 | $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyRead 398 | || 399 | $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyWrite 400 | || 401 | $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Property 402 | ) { 403 | $propertyPhp = new PHPProperty($this->parserContainer); 404 | 405 | $nameTmp = $parsedPropertyTag->getVariableName(); 406 | if (!$nameTmp) { 407 | continue; 408 | } 409 | 410 | $propertyPhp->name = $nameTmp; 411 | 412 | $propertyPhp->access = 'public'; 413 | 414 | $type = $parsedPropertyTag->getType(); 415 | 416 | $propertyPhp->typeFromPhpDoc = Utils::normalizePhpType($type . ''); 417 | 418 | $typeFromPhpDocMaybeWithCommentTmp = \trim((string) $parsedPropertyTag); 419 | if ( 420 | $typeFromPhpDocMaybeWithCommentTmp 421 | && 422 | \strpos($typeFromPhpDocMaybeWithCommentTmp, '$') !== 0 423 | ) { 424 | $propertyPhp->typeFromPhpDocMaybeWithComment = $typeFromPhpDocMaybeWithCommentTmp; 425 | } 426 | 427 | $typeTmp = Utils::parseDocTypeObject($type); 428 | if ($typeTmp !== '') { 429 | $propertyPhp->typeFromPhpDocSimple = $typeTmp; 430 | } 431 | 432 | if ($propertyPhp->typeFromPhpDoc) { 433 | $propertyPhp->typeFromPhpDocExtended = Utils::modernPhpdoc($propertyPhp->typeFromPhpDoc); 434 | } 435 | 436 | $this->properties[$propertyPhp->name] = $propertyPhp; 437 | } 438 | } 439 | } 440 | } catch (\Exception $e) { 441 | $tmpErrorMessage = ($this->name ?: '?') . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true); 442 | $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage; 443 | } 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Parsers/Helper/Utils.php: -------------------------------------------------------------------------------- 1 | [^ ]*)#u', $parsedParamTagStr, $variableNameHelper); 42 | if (isset($variableNameHelper['variableName'])) { 43 | $variableName = $variableNameHelper['variableName']; 44 | } 45 | $parsedParamTagStr = \str_replace( 46 | (string) $variableName, 47 | '', 48 | $parsedParamTagStr 49 | ); 50 | } 51 | 52 | // clean-up 53 | if ($variableName) { 54 | $variableName = \str_replace('$', '', $variableName); 55 | } 56 | 57 | $parsedParamTagStr = \trim($parsedParamTagStr); 58 | 59 | return [ 60 | 'parsedParamTagStr' => $parsedParamTagStr, 61 | 'variableName' => $variableName, 62 | ]; 63 | } 64 | 65 | /** 66 | * @param \PhpParser\Node\Arg|\PhpParser\Node\Const_|\PhpParser\Node\Expr $node 67 | * @param string|null $classStr 68 | * @param \voku\SimplePhpParser\Parsers\Helper\ParserContainer|null $parserContainer 69 | * 70 | * @phpstan-param class-string|null $classStr 71 | * 72 | * @return mixed 73 | * Will return "Utils::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER" if we can't get the default value 74 | */ 75 | public static function getPhpParserValueFromNode( 76 | $node, 77 | ?string $classStr = null, 78 | ?ParserContainer $parserContainer = null 79 | ) { 80 | if (\property_exists($node, 'value')) { 81 | /** @psalm-suppress UndefinedPropertyFetch - false-positive ? */ 82 | if (\is_object($node->value)) { 83 | \assert($node->value instanceof \PhpParser\Node); 84 | 85 | if ( 86 | \in_array('value', $node->value->getSubNodeNames(), true) 87 | && 88 | \property_exists($node->value, 'value') 89 | ) { 90 | return $node->value->value; 91 | } 92 | 93 | if ( 94 | \in_array('expr', $node->value->getSubNodeNames(), true) 95 | && 96 | \property_exists($node->value, 'expr') 97 | ) { 98 | $exprTmp = $node->value->expr; 99 | if (\property_exists($exprTmp, 'value')) { 100 | if ($node->value instanceof UnaryMinus) { 101 | return -$exprTmp->value; 102 | } 103 | 104 | return $exprTmp->value; 105 | } 106 | } 107 | 108 | if ( 109 | \in_array('name', $node->value->getSubNodeNames(), true) 110 | && 111 | \property_exists($node->value, 'name') 112 | && 113 | $node->value->name 114 | ) { 115 | $value = implode('\\', $node->value->name->getParts()) ?: $node->value->name->name; 116 | return $value === 'null' ? null : $value; 117 | } 118 | } 119 | 120 | /** 121 | * @psalm-suppress UndefinedPropertyFetch - false-positive from psalm 122 | */ 123 | return $node->value; 124 | } 125 | 126 | if ($node instanceof \PhpParser\Node\Expr\Array_) { 127 | $defaultValue = []; 128 | foreach ($node->items as $item) { 129 | if ($item === null) { 130 | continue; 131 | } 132 | 133 | $defaultValue[] = self::getPhpParserValueFromNode($item->value); 134 | } 135 | 136 | return $defaultValue; 137 | } 138 | 139 | if ($node instanceof \PhpParser\Node\Expr\ClassConstFetch) { 140 | \assert($node->class instanceof \PhpParser\Node\Name); 141 | \assert($node->name instanceof \PhpParser\Node\Identifier); 142 | $className = $node->class->toString(); 143 | $constantName = $node->name->name; 144 | 145 | if ($className === 'self' || $className === 'static') { 146 | if ($classStr === null || $parserContainer === null) { 147 | return self::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER; 148 | } 149 | 150 | $className = self::findParentClassDeclaringConstant($classStr, $constantName, $parserContainer); 151 | } 152 | 153 | $className = '\\' . \ltrim($className, '\\'); 154 | 155 | if ($node->name->name === 'class') { 156 | return $className; 157 | } 158 | 159 | if (\class_exists($className, true)) { 160 | return \constant($className . '::' . $node->name->name); 161 | } 162 | } 163 | 164 | if ($node instanceof \PhpParser\Node\Expr\ConstFetch) { 165 | $parts = $node->name->getParts(); 166 | 167 | $returnTmp = \strtolower($parts[0]); 168 | if ($returnTmp === 'true') { 169 | return true; 170 | } 171 | if ($returnTmp === 'false') { 172 | return false; 173 | } 174 | if ($returnTmp === 'null') { 175 | return null; 176 | } 177 | 178 | $constantNameTmp = '\\' . \implode('\\', $parts); 179 | if (\defined($constantNameTmp)) { 180 | return \constant($constantNameTmp); 181 | } 182 | } 183 | 184 | return self::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER; 185 | } 186 | 187 | public static function normalizePhpType(string $type_string, bool $sort = false): ?string 188 | { 189 | $type_string_lower = \strtolower($type_string); 190 | 191 | /** @noinspection PhpSwitchCaseWithoutDefaultBranchInspection */ 192 | switch ($type_string_lower) { 193 | case 'int': 194 | case 'void': 195 | case 'float': 196 | case 'string': 197 | case 'bool': 198 | case 'callable': 199 | case 'iterable': 200 | case 'array': 201 | case 'object': 202 | case 'true': 203 | case 'false': 204 | case 'null': 205 | case 'mixed': 206 | return $type_string_lower; 207 | } 208 | 209 | /** @noinspection PhpSwitchCaseWithoutDefaultBranchInspection */ 210 | switch ($type_string_lower) { 211 | case 'boolean': 212 | return 'bool'; 213 | 214 | case 'integer': 215 | return 'int'; 216 | 217 | case 'double': 218 | case 'real': 219 | return 'float'; 220 | } 221 | 222 | if ($type_string === '') { 223 | return null; 224 | } 225 | 226 | if ($sort && \strpos($type_string, '|') !== false) { 227 | $type_string_exploded = \explode('|', $type_string); 228 | sort($type_string_exploded); 229 | $type_string = \implode('|', $type_string_exploded); 230 | } 231 | 232 | return $type_string; 233 | } 234 | 235 | /** 236 | * @param \phpDocumentor\Reflection\Type|\phpDocumentor\Reflection\Type[]|null $type 237 | * 238 | * @return string 239 | * 240 | * @psalm-suppress InvalidReturnType - false-positive from psalm 241 | */ 242 | public static function parseDocTypeObject($type): string 243 | { 244 | if ($type === null) { 245 | return ''; 246 | } 247 | 248 | if ($type instanceof \phpDocumentor\Reflection\Types\Object_) { 249 | return $type->__toString(); 250 | } 251 | 252 | if (\is_array($type) || $type instanceof \phpDocumentor\Reflection\Types\Compound) { 253 | $types = []; 254 | foreach ($type as $subType) { 255 | $types[] = self::parseDocTypeObject($subType); 256 | } 257 | 258 | /** 259 | * @psalm-suppress InvalidReturnStatement - false-positive from psalm 260 | */ 261 | return \implode('|', $types); 262 | } 263 | 264 | if ($type instanceof \phpDocumentor\Reflection\Types\Array_) { 265 | $valueTypeTmp = $type->getValueType() . ''; 266 | if ($valueTypeTmp !== 'mixed') { 267 | if (\strpos($valueTypeTmp, '|') !== false) { 268 | $valueTypeTmpExploded = \explode('|', $valueTypeTmp); 269 | $valueTypeTmp = ''; 270 | foreach ($valueTypeTmpExploded as $valueTypeTmpExplodedInner) { 271 | $valueTypeTmp .= $valueTypeTmpExplodedInner . '[]|'; 272 | } 273 | 274 | return \rtrim($valueTypeTmp, '|'); 275 | } 276 | 277 | return $valueTypeTmp . '[]'; 278 | } 279 | 280 | return 'array'; 281 | } 282 | 283 | if ($type instanceof \phpDocumentor\Reflection\Types\Null_) { 284 | return 'null'; 285 | } 286 | 287 | if ($type instanceof \phpDocumentor\Reflection\Types\Mixed_) { 288 | return 'mixed'; 289 | } 290 | 291 | if ($type instanceof \phpDocumentor\Reflection\Types\Scalar) { 292 | return 'string|int|float|bool'; 293 | } 294 | 295 | if ($type instanceof \phpDocumentor\Reflection\PseudoTypes\True_) { 296 | return 'true'; 297 | } 298 | 299 | if ($type instanceof \phpDocumentor\Reflection\PseudoTypes\False_) { 300 | return 'false'; 301 | } 302 | 303 | if ($type instanceof \phpDocumentor\Reflection\Types\Boolean) { 304 | return 'bool'; 305 | } 306 | 307 | if ($type instanceof \phpDocumentor\Reflection\Types\Callable_) { 308 | return 'callable'; 309 | } 310 | 311 | if ($type instanceof \phpDocumentor\Reflection\Types\Float_) { 312 | return 'float'; 313 | } 314 | 315 | if ($type instanceof \phpDocumentor\Reflection\Types\String_) { 316 | return 'string'; 317 | } 318 | 319 | if ($type instanceof \phpDocumentor\Reflection\Types\Integer) { 320 | return 'int'; 321 | } 322 | 323 | if ($type instanceof \phpDocumentor\Reflection\Types\Void_) { 324 | return 'void'; 325 | } 326 | 327 | if ($type instanceof \phpDocumentor\Reflection\Types\Resource_) { 328 | return 'resource'; 329 | } 330 | 331 | return $type . ''; 332 | } 333 | 334 | public static function createFunctionReflectionInstance(string $functionName): ReflectionFunction 335 | { 336 | static $FUNCTION_REFLECTION_INSTANCE = []; 337 | 338 | if (isset($FUNCTION_REFLECTION_INSTANCE[$functionName])) { 339 | return $FUNCTION_REFLECTION_INSTANCE[$functionName]; 340 | } 341 | 342 | $reflection = new \ReflectionFunction($functionName); 343 | \assert($reflection instanceof \ReflectionFunction); 344 | 345 | $FUNCTION_REFLECTION_INSTANCE[$functionName] = $reflection; 346 | 347 | return $reflection; 348 | } 349 | 350 | /** 351 | * @phpstan-param class-string $className 352 | */ 353 | public static function createClassReflectionInstance(string $className): ReflectionClass 354 | { 355 | static $CLASS_REFLECTION_INSTANCE = []; 356 | 357 | if (isset($CLASS_REFLECTION_INSTANCE[$className])) { 358 | return $CLASS_REFLECTION_INSTANCE[$className]; 359 | } 360 | 361 | $reflection = new ReflectionClass($className); 362 | \assert($reflection instanceof ReflectionClass); 363 | 364 | $CLASS_REFLECTION_INSTANCE[$className] = $reflection; 365 | 366 | return $reflection; 367 | } 368 | 369 | /** 370 | * Factory method for easy instantiation. 371 | * 372 | * @param string[] $additionalTags 373 | * 374 | * @phpstan-param array> $additionalTags 375 | */ 376 | public static function createDocBlockInstance(array $additionalTags = []): \phpDocumentor\Reflection\DocBlockFactory 377 | { 378 | static $DOC_BLOCK_FACTORY_INSTANCE = null; 379 | 380 | if ($DOC_BLOCK_FACTORY_INSTANCE !== null) { 381 | return $DOC_BLOCK_FACTORY_INSTANCE; 382 | } 383 | 384 | $fqsenResolver = new \phpDocumentor\Reflection\FqsenResolver(); 385 | $tagFactory = new \phpDocumentor\Reflection\DocBlock\StandardTagFactory($fqsenResolver); 386 | $descriptionFactory = new \phpDocumentor\Reflection\DocBlock\DescriptionFactory($tagFactory); 387 | $typeResolver = new \phpDocumentor\Reflection\TypeResolver($fqsenResolver); 388 | 389 | /** 390 | * @psalm-suppress InvalidArgument - false-positive from "ReflectionDocBlock" + PHP >= 7.2 391 | */ 392 | $tagFactory->addService($descriptionFactory); 393 | 394 | /** 395 | * @psalm-suppress InvalidArgument - false-positive from "ReflectionDocBlock" + PHP >= 7.2 396 | */ 397 | $tagFactory->addService($typeResolver); 398 | 399 | $docBlockFactory = new \phpDocumentor\Reflection\DocBlockFactory($descriptionFactory, $tagFactory); 400 | foreach ($additionalTags as $tagName => $tagHandler) { 401 | $docBlockFactory->registerTagHandler($tagName, $tagHandler); 402 | } 403 | 404 | $DOC_BLOCK_FACTORY_INSTANCE = $docBlockFactory; 405 | 406 | return $docBlockFactory; 407 | } 408 | 409 | public static function modernPhpdocTokens(string $input): \PHPStan\PhpDocParser\Parser\TokenIterator 410 | { 411 | static $LAXER = null; 412 | 413 | if ($LAXER === null) { 414 | $LAXER = new \PHPStan\PhpDocParser\Lexer\Lexer(); 415 | } 416 | 417 | return new \PHPStan\PhpDocParser\Parser\TokenIterator($LAXER->tokenize($input)); 418 | } 419 | 420 | /** 421 | * @throws \PHPStan\PhpDocParser\Parser\ParserException 422 | */ 423 | public static function modernPhpdoc(string $input): string 424 | { 425 | static $TYPE_PARSER = null; 426 | 427 | if ($TYPE_PARSER === null) { 428 | $TYPE_PARSER = new \PHPStan\PhpDocParser\Parser\TypeParser(new \PHPStan\PhpDocParser\Parser\ConstExprParser()); 429 | } 430 | 431 | $tokens = self::modernPhpdocTokens($input); 432 | $typeNode = $TYPE_PARSER->parse($tokens); 433 | 434 | return \str_replace( 435 | [ 436 | ' | ', 437 | ], 438 | [ 439 | '|', 440 | ], 441 | \trim((string) $typeNode, ')(') 442 | ); 443 | } 444 | 445 | /** 446 | * @see https://gist.github.com/divinity76/01ef9ca99c111565a72d3a8a6e42f7fb + modified (do not use all cores, we still want to work) 447 | * 448 | * returns number of cpu cores 449 | * Copyleft 2018, license: WTFPL 450 | * 451 | * @return int<1, max> 452 | */ 453 | public static function getCpuCores(): int 454 | { 455 | if (\defined('PHP_WINDOWS_VERSION_MAJOR')) { 456 | $str = \trim((string) \shell_exec('wmic cpu get NumberOfCores 2>&1')); 457 | $matches = []; 458 | if (!$str || !\preg_match('#(\d+)#', $str, $matches)) { 459 | return 1; 460 | } 461 | 462 | $return = (int)round((int)$matches[1] / 2); 463 | if ($return > 1) { 464 | return $return; 465 | } 466 | 467 | return 1; 468 | } 469 | 470 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 471 | $ret = @\shell_exec('nproc'); 472 | if (\is_string($ret)) { 473 | $ret = \trim($ret); 474 | /** @noinspection PhpAssignmentInConditionInspection */ 475 | if ($ret && ($tmp = \filter_var($ret, \FILTER_VALIDATE_INT)) !== false) { 476 | $return = (int)round($tmp / 2); 477 | if ($return > 1) { 478 | return $return; 479 | } 480 | 481 | return 1; 482 | } 483 | } 484 | 485 | if (\is_readable('/proc/cpuinfo')) { 486 | $cpuinfo = (string) \file_get_contents('/proc/cpuinfo'); 487 | $count = \substr_count($cpuinfo, 'processor'); 488 | if ($count > 0) { 489 | $return = (int)round($count / 2); 490 | if ($return > 1) { 491 | return $return; 492 | } 493 | 494 | return 1; 495 | } 496 | } 497 | 498 | return 1; 499 | } 500 | 501 | private static function findParentClassDeclaringConstant( 502 | string $classStr, 503 | string $constantName, 504 | ParserContainer $parserContainer 505 | ): string { 506 | do { 507 | $class = $parserContainer->getClass($classStr); 508 | if ($class && $class->name && isset($class->constants[$constantName])) { 509 | return $class->name; 510 | } 511 | 512 | if ($class && $class->parentClass) { 513 | $class = $parserContainer->getClass($class->parentClass); 514 | } 515 | } while ($class); 516 | 517 | return $classStr; 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/voku/SimplePhpParser/Parsers/PhpCodeParser.php: -------------------------------------------------------------------------------- 1 | getFileName(), 65 | $autoloaderProjectPaths 66 | ); 67 | } 68 | 69 | /** 70 | * @param string $pathOrCode 71 | * @param string[] $autoloaderProjectPaths 72 | * @param string[] $pathExcludeRegex 73 | * @param string[] $fileExtensions 74 | * 75 | * @return \voku\SimplePhpParser\Parsers\Helper\ParserContainer 76 | */ 77 | public static function getPhpFiles( 78 | string $pathOrCode, 79 | array $autoloaderProjectPaths = [], 80 | array $pathExcludeRegex = [], 81 | array $fileExtensions = [] 82 | ): ParserContainer { 83 | foreach ($autoloaderProjectPaths as $projectPath) { 84 | if (\file_exists($projectPath) && \is_file($projectPath)) { 85 | require_once $projectPath; 86 | } elseif (\file_exists($projectPath . '/vendor/autoload.php')) { 87 | require_once $projectPath . '/vendor/autoload.php'; 88 | } elseif (\file_exists($projectPath . '/../vendor/autoload.php')) { 89 | require_once $projectPath . '/../vendor/autoload.php'; 90 | } 91 | } 92 | \restore_error_handler(); 93 | 94 | $phpCodes = self::getCode( 95 | $pathOrCode, 96 | $pathExcludeRegex, 97 | $fileExtensions 98 | ); 99 | 100 | $parserContainer = new ParserContainer(); 101 | $visitor = new ASTVisitor($parserContainer); 102 | 103 | $processResults = []; 104 | $phpCodesChunks = \array_chunk($phpCodes, Utils::getCpuCores(), true); 105 | 106 | foreach ($phpCodesChunks as $phpCodesChunk) { 107 | foreach ($phpCodesChunk as $codeAndFileName) { 108 | $processResults[] = self::process( 109 | $codeAndFileName['content'], 110 | $codeAndFileName['fileName'], 111 | $parserContainer, 112 | $visitor 113 | ); 114 | } 115 | } 116 | 117 | foreach ($processResults as $response) { 118 | if ($response instanceof ParserContainer) { 119 | $parserContainer->setTraits($response->getTraits()); 120 | $parserContainer->setClasses($response->getClasses()); 121 | $parserContainer->setInterfaces($response->getInterfaces()); 122 | $parserContainer->setConstants($response->getConstants()); 123 | $parserContainer->setFunctions($response->getFunctions()); 124 | } elseif ($response instanceof ParserErrorHandler) { 125 | $parserContainer->setParseError($response); 126 | } 127 | } 128 | 129 | $interfaces = $parserContainer->getInterfaces(); 130 | foreach ($interfaces as &$interface) { 131 | $interface->parentInterfaces = $visitor->combineParentInterfaces($interface); 132 | } 133 | unset($interface); 134 | 135 | $pathTmp = null; 136 | if (\is_file($pathOrCode)) { 137 | $pathTmp = \realpath(\pathinfo($pathOrCode, \PATHINFO_DIRNAME)); 138 | } elseif (\is_dir($pathOrCode)) { 139 | $pathTmp = \realpath($pathOrCode); 140 | } 141 | 142 | $classesTmp = &$parserContainer->getClassesByReference(); 143 | foreach ($classesTmp as &$classTmp) { 144 | $classTmp->interfaces = Utils::flattenArray( 145 | $visitor->combineImplementedInterfaces($classTmp), 146 | false 147 | ); 148 | 149 | self::mergeInheritdocData( 150 | $classTmp, 151 | $classesTmp, 152 | $interfaces, 153 | $parserContainer 154 | ); 155 | } 156 | unset($classTmp); 157 | 158 | // remove properties / methods / classes from outside of the current file-path-scope 159 | if ($pathTmp) { 160 | $classesTmp2 = &$parserContainer->getClassesByReference(); 161 | foreach ($classesTmp2 as $classKey => $classTmp2) { 162 | foreach ($classTmp2->constants as $constantKey => $constant) { 163 | if ($constant->file && \strpos($constant->file, $pathTmp) === false) { 164 | unset($classTmp2->constants[$constantKey]); 165 | } 166 | } 167 | 168 | foreach ($classTmp2->properties as $propertyKey => $property) { 169 | if ($property->file && \strpos($property->file, $pathTmp) === false) { 170 | unset($classTmp2->properties[$propertyKey]); 171 | } 172 | } 173 | 174 | foreach ($classTmp2->methods as $methodKey => $method) { 175 | if ($method->file && \strpos($method->file, $pathTmp) === false) { 176 | unset($classTmp2->methods[$methodKey]); 177 | } 178 | } 179 | 180 | if ($classTmp2->file && \strpos($classTmp2->file, $pathTmp) === false) { 181 | unset($classesTmp2[$classKey]); 182 | } 183 | } 184 | } 185 | 186 | return $parserContainer; 187 | } 188 | 189 | /** 190 | * @param string $phpCode 191 | * @param string|null $fileName 192 | * @param \voku\SimplePhpParser\Parsers\Helper\ParserContainer $parserContainer 193 | * @param \voku\SimplePhpParser\Parsers\Visitors\ASTVisitor $visitor 194 | * 195 | * @return \voku\SimplePhpParser\Parsers\Helper\ParserContainer|\voku\SimplePhpParser\Parsers\Helper\ParserErrorHandler 196 | */ 197 | public static function process( 198 | string $phpCode, 199 | ?string $fileName, 200 | ParserContainer $parserContainer, 201 | ASTVisitor $visitor 202 | ) { 203 | $parser = (new ParserFactory())->create( 204 | ParserFactory::PREFER_PHP7, 205 | new Emulative( 206 | [ 207 | 'usedAttributes' => [ 208 | 'comments', 209 | 'startLine', 210 | 'endLine', 211 | 'startTokenPos', 212 | 'endTokenPos', 213 | ], 214 | ] 215 | ) 216 | ); 217 | 218 | $errorHandler = new ParserErrorHandler(); 219 | 220 | $nameResolver = new NameResolver( 221 | $errorHandler, 222 | [ 223 | 'preserveOriginalNames' => true, 224 | ] 225 | ); 226 | 227 | /** @var \PhpParser\Node[]|null $parsedCode */ 228 | $parsedCode = $parser->parse($phpCode, $errorHandler); 229 | 230 | if ($parsedCode === null) { 231 | return $errorHandler; 232 | } 233 | 234 | $visitor->fileName = $fileName; 235 | 236 | $traverser = new NodeTraverser(); 237 | $traverser->addVisitor(new ParentConnector()); 238 | $traverser->addVisitor($nameResolver); 239 | $traverser->addVisitor($visitor); 240 | $traverser->traverse($parsedCode); 241 | 242 | return $parserContainer; 243 | } 244 | 245 | /** 246 | * @param string $pathOrCode 247 | * @param string[] $pathExcludeRegex 248 | * @param string[] $fileExtensions 249 | * 250 | * @return array 251 | * 252 | * @psalm-return array 253 | */ 254 | private static function getCode( 255 | string $pathOrCode, 256 | array $pathExcludeRegex = [], 257 | array $fileExtensions = [] 258 | ): array { 259 | // init 260 | $phpCodes = []; 261 | /** @var SplFileInfo[] $phpFileIterators */ 262 | $phpFileIterators = []; 263 | /** @var \React\Promise\PromiseInterface[] $phpFilePromises */ 264 | $phpFilePromises = []; 265 | 266 | // fallback 267 | if (\count($fileExtensions) === 0) { 268 | $fileExtensions = ['.php']; 269 | } 270 | 271 | if (\is_file($pathOrCode)) { 272 | $phpFileIterators = [new SplFileInfo($pathOrCode)]; 273 | } elseif (\is_dir($pathOrCode)) { 274 | $phpFileIterators = new RecursiveIteratorIterator( 275 | new RecursiveDirectoryIterator($pathOrCode, FilesystemIterator::SKIP_DOTS) 276 | ); 277 | } else { 278 | $cacheKey = self::CACHE_KEY_HELPER . \md5($pathOrCode); 279 | 280 | $phpCodes[$cacheKey]['content'] = $pathOrCode; 281 | $phpCodes[$cacheKey]['fileName'] = null; 282 | } 283 | 284 | $cache = new Cache(null, null, false); 285 | 286 | $phpFileArray = []; 287 | foreach ($phpFileIterators as $fileOrCode) { 288 | $path = $fileOrCode->getRealPath(); 289 | if (!$path) { 290 | continue; 291 | } 292 | 293 | $fileExtensionFound = false; 294 | foreach ($fileExtensions as $fileExtension) { 295 | if (\substr($path, -\strlen($fileExtension)) === $fileExtension) { 296 | $fileExtensionFound = true; 297 | 298 | break; 299 | } 300 | } 301 | if ($fileExtensionFound === false) { 302 | continue; 303 | } 304 | 305 | foreach ($pathExcludeRegex as $regex) { 306 | if (\preg_match($regex, $path)) { 307 | continue 2; 308 | } 309 | } 310 | 311 | $cacheKey = self::CACHE_KEY_HELPER . \md5($path) . '--' . \filemtime($path); 312 | if ($cache->getCacheIsReady() === true && $cache->existsItem($cacheKey)) { 313 | $response = $cache->getItem($cacheKey); 314 | /** @noinspection PhpSillyAssignmentInspection - helper for phpstan */ 315 | /** @phpstan-var array{content: string, fileName: string, cacheKey: string} $response */ 316 | $response = $response; 317 | 318 | $phpCodes[$response['cacheKey']]['content'] = $response['content']; 319 | $phpCodes[$response['cacheKey']]['fileName'] = $response['fileName']; 320 | 321 | continue; 322 | } 323 | 324 | $phpFileArray[$cacheKey] = $path; 325 | } 326 | 327 | $phpFileArrayChunks = \array_chunk($phpFileArray, Utils::getCpuCores(), true); 328 | foreach ($phpFileArrayChunks as $phpFileArrayChunk) { 329 | $filesystem = \React\Filesystem\Factory::create(); 330 | 331 | foreach ($phpFileArrayChunk as $cacheKey => $path) { 332 | $phpFilePromises[] = $filesystem->detect($path)->then( 333 | function (FileInterface $file) use ($path, $cacheKey) { 334 | return [ 335 | 'content' => $file->getContents()->then(static function (string $contents) { 336 | return $contents; 337 | }), 338 | 'fileName' => $path, 339 | 'cacheKey' => $cacheKey, 340 | ]; 341 | }, 342 | function ($e) { 343 | throw $e; 344 | } 345 | ); 346 | } 347 | 348 | $phpFilePromiseResponses = await(all($phpFilePromises)); 349 | foreach ($phpFilePromiseResponses as $response) { 350 | $response['content'] = await($response['content']); 351 | 352 | assert(is_string($response['content'])); 353 | assert(is_string($response['cacheKey'])); 354 | assert($response['fileName'] === null || is_string($response['fileName'])); 355 | 356 | $cache->setItem($response['cacheKey'], $response); 357 | 358 | $phpCodes[$response['cacheKey']]['content'] = $response['content']; 359 | $phpCodes[$response['cacheKey']]['fileName'] = $response['fileName']; 360 | } 361 | } 362 | 363 | return $phpCodes; 364 | } 365 | 366 | /** 367 | * @param \voku\SimplePhpParser\Model\PHPClass $class 368 | * @param \voku\SimplePhpParser\Model\PHPClass[] $classes 369 | * @param PHPInterface[] $interfaces 370 | * @param ParserContainer $parserContainer 371 | */ 372 | private static function mergeInheritdocData( 373 | \voku\SimplePhpParser\Model\PHPClass $class, 374 | array $classes, 375 | array $interfaces, 376 | ParserContainer $parserContainer 377 | ): void { 378 | foreach ($class->properties as &$property) { 379 | if (!$class->parentClass) { 380 | break; 381 | } 382 | 383 | if (!$property->is_inheritdoc) { 384 | continue; 385 | } 386 | 387 | if ( 388 | !isset($classes[$class->parentClass]) 389 | && 390 | \class_exists($class->parentClass, true) 391 | ) { 392 | $reflectionClassTmp = Utils::createClassReflectionInstance($class->parentClass); 393 | $classTmp = (new \voku\SimplePhpParser\Model\PHPClass($parserContainer))->readObjectFromReflection($reflectionClassTmp); 394 | if ($classTmp->name) { 395 | $classes[$classTmp->name] = $classTmp; 396 | } 397 | } 398 | 399 | if (!isset($classes[$class->parentClass])) { 400 | continue; 401 | } 402 | 403 | if (!isset($classes[$class->parentClass]->properties[$property->name])) { 404 | continue; 405 | } 406 | 407 | $parentMethod = $classes[$class->parentClass]->properties[$property->name]; 408 | 409 | foreach ($property as $key => &$value) { 410 | if ( 411 | $value === null 412 | && 413 | $parentMethod->{$key} !== null 414 | && 415 | \stripos($key, 'type') !== false 416 | ) { 417 | $value = $parentMethod->{$key}; 418 | } 419 | } 420 | } 421 | unset($property, $value); /* @phpstan-ignore-line ? */ 422 | 423 | foreach ($class->methods as &$method) { 424 | if (!$method->is_inheritdoc) { 425 | continue; 426 | } 427 | 428 | foreach ($class->interfaces as $interfaceStr) { 429 | if ( 430 | !isset($interfaces[$interfaceStr]) 431 | && 432 | \interface_exists($interfaceStr, true) 433 | ) { 434 | $reflectionInterfaceTmp = Utils::createClassReflectionInstance($interfaceStr); 435 | $interfaceTmp = (new PHPInterface($parserContainer))->readObjectFromReflection($reflectionInterfaceTmp); 436 | if ($interfaceTmp->name) { 437 | $interfaces[$interfaceTmp->name] = $interfaceTmp; 438 | } 439 | } 440 | 441 | if (!isset($interfaces[$interfaceStr])) { 442 | continue; 443 | } 444 | 445 | if (!isset($interfaces[$interfaceStr]->methods[$method->name])) { 446 | continue; 447 | } 448 | 449 | $interfaceMethod = $interfaces[$interfaceStr]->methods[$method->name]; 450 | 451 | foreach ($method as $key => &$value) { 452 | if ( 453 | $value === null 454 | && 455 | $interfaceMethod->{$key} !== null 456 | && 457 | \stripos($key, 'type') !== false 458 | ) { 459 | $value = $interfaceMethod->{$key}; 460 | } 461 | 462 | if ($key === 'parameters') { 463 | $parameterCounter = 0; 464 | foreach ($value as &$parameter) { 465 | ++$parameterCounter; 466 | 467 | \assert($parameter instanceof \voku\SimplePhpParser\Model\PHPParameter); 468 | 469 | $interfaceMethodParameter = null; 470 | $parameterCounterInterface = 0; 471 | foreach ($interfaceMethod->parameters as $parameterInterface) { 472 | ++$parameterCounterInterface; 473 | 474 | if ($parameterCounterInterface === $parameterCounter) { 475 | $interfaceMethodParameter = $parameterInterface; 476 | } 477 | } 478 | 479 | if (!$interfaceMethodParameter) { 480 | continue; 481 | } 482 | 483 | foreach ($parameter as $keyInner => &$valueInner) { 484 | if ( 485 | $valueInner === null 486 | && 487 | $interfaceMethodParameter->{$keyInner} !== null 488 | && 489 | \stripos($keyInner, 'type') !== false 490 | ) { 491 | $valueInner = $interfaceMethodParameter->{$keyInner}; 492 | } 493 | } 494 | unset($valueInner); /* @phpstan-ignore-line ? */ 495 | } 496 | unset($parameter); 497 | } 498 | } 499 | unset($value); /* @phpstan-ignore-line ? */ 500 | } 501 | 502 | if (!isset($classes[$class->parentClass])) { 503 | continue; 504 | } 505 | 506 | if (!isset($classes[$class->parentClass]->methods[$method->name])) { 507 | continue; 508 | } 509 | 510 | $parentMethod = $classes[$class->parentClass]->methods[$method->name]; 511 | 512 | foreach ($method as $key => &$value) { 513 | if ( 514 | $value === null 515 | && 516 | $parentMethod->{$key} !== null 517 | && 518 | \stripos($key, 'type') !== false 519 | ) { 520 | $value = $parentMethod->{$key}; 521 | } 522 | 523 | if ($key === 'parameters') { 524 | $parameterCounter = 0; 525 | foreach ($value as &$parameter) { 526 | ++$parameterCounter; 527 | 528 | \assert($parameter instanceof \voku\SimplePhpParser\Model\PHPParameter); 529 | 530 | $parentMethodParameter = null; 531 | $parameterCounterParent = 0; 532 | foreach ($parentMethod->parameters as $parameterParent) { 533 | ++$parameterCounterParent; 534 | 535 | if ($parameterCounterParent === $parameterCounter) { 536 | $parentMethodParameter = $parameterParent; 537 | } 538 | } 539 | 540 | if (!$parentMethodParameter) { 541 | continue; 542 | } 543 | 544 | foreach ($parameter as $keyInner => &$valueInner) { 545 | if ( 546 | $valueInner === null 547 | && 548 | $parentMethodParameter->{$keyInner} !== null 549 | && 550 | \stripos($keyInner, 'type') !== false 551 | ) { 552 | $valueInner = $parentMethodParameter->{$keyInner}; 553 | } 554 | } 555 | } 556 | } 557 | } 558 | } 559 | } 560 | } 561 | --------------------------------------------------------------------------------