diff --git a/app/Contexts/Variable.php b/app/Contexts/Variable.php new file mode 100644 index 0000000..81450b0 --- /dev/null +++ b/app/Contexts/Variable.php @@ -0,0 +1,25 @@ + $this->name, + 'className' => $this->className, + ]; + } +} diff --git a/app/Parsers/MethodDeclarationParser.php b/app/Parsers/MethodDeclarationParser.php index 7dfca17..18741df 100644 --- a/app/Parsers/MethodDeclarationParser.php +++ b/app/Parsers/MethodDeclarationParser.php @@ -17,6 +17,11 @@ public function parse(MethodDeclaration $node) { $this->context->methodName = $node->getName(); + // Every method is a new context, so we need to clear + // the previous variable contexts + // @see https://github.com/laravel/vs-code-php-parser-cli/pull/14 + VariableParser::$previousContexts = []; + return $this->context; } diff --git a/app/Parsers/VariableParser.php b/app/Parsers/VariableParser.php new file mode 100644 index 0000000..0cd5681 --- /dev/null +++ b/app/Parsers/VariableParser.php @@ -0,0 +1,174 @@ +getDocCommentText(); + + if ($docComment === null && $node->getParent() !== null) { + return $this->getLatestDocComment($node->getParent()); + } + + return $docComment; + } + + private function searchClassNameInParameter(): ?string + { + $className = $this->context->searchForVar($this->context->name); + + return is_string($className) ? $className : null; + } + + private function searchClassNameInDocComment(Node $node): ?string + { + $docComment = $this->getLatestDocComment($node); + + if ($docComment === null) { + return null; + } + + $config = new ParserConfig([]); + $lexer = new Lexer($config); + $phpDocParser = $this->createPhpDocParser($config); + + $tokens = new TokenIterator($lexer->tokenize($docComment)); + $phpDocNode = $phpDocParser->parse($tokens); + + $varTagValues = $phpDocNode->getVarTagValues(); + + /** @var VarTagValueNode|null $varTagValue */ + $varTagValue = collect($varTagValues) + // We need to remove first character because it's always $ + ->first(fn (VarTagValueNode $valueNode) => substr($valueNode->variableName, 1) === $this->context->name); + + if (! $varTagValue?->type instanceof IdentifierTypeNode) { + return null; + } + + // If the class name starts with a backslash, it's a fully qualified name + if (str_starts_with($varTagValue->type->name, '\\')) { + return substr($varTagValue->type->name, 1); + } + + // Otherwise, it's a short name and we need to find the fully qualified name from + // the imported namespaces + $uses = []; + + foreach ($node->getRoot()->getDescendantNodes() as $node) { + if (! $node instanceof NamespaceUseDeclaration) { + continue; + } + + foreach ($node->useClauses->children ?? [] as $clause) { + if (! $clause instanceof NamespaceUseClause) { + continue; + } + + $fqcn = $clause->namespaceName->getText(); + + // If the namespace has an alias, we need to use the alias as the short name + $alias = $clause->namespaceAliasingClause + ? str($clause->namespaceAliasingClause->getText()) + ->after('as') + ->trim() + ->toString() + : str($fqcn)->explode('\\')->last(); + + // Finally, we add the short and fully qualified name to the uses array + $uses[$alias] = $fqcn; + } + } + + return $uses[$varTagValue->type->name] ?? null; + } + + private function searchClassNameInPreviousContexts(): ?string + { + /** @var VariableContext|null $previousVariableContext */ + $previousVariableContext = collect(self::$previousContexts) + ->last(fn (VariableContext $context) => $context->name === $this->context->name); + + return $previousVariableContext?->className; + } + + public function parse(Variable $node) + { + $this->context->name = $node->getName(); + + // Firstly, we try to find the className from the method parameter + $this->context->className = $this->searchClassNameInParameter() + // If the className is still not found, we try to find the className + // from the doc comment, for example: + // + // /** @var \App\Models\User $user */ + // Gate::allows('edit', $user); + ?? $this->searchClassNameInDocComment($node) + // If the className is still not found, we try to find the className + // from the previous variable contexts, for example: + // + // /** @var \App\Models\User $user */ + // $user = $request->user; + // + // Gate::allows('edit', $user); + ?? $this->searchClassNameInPreviousContexts(); + + if (Settings::$capturePosition) { + $range = PositionUtilities::getRangeFromPosition( + $node->getStartPosition(), + mb_strlen($node->getText()), + $node->getRoot()->getFullText(), + ); + + if (Settings::$calculatePosition !== null) { + $range = Settings::adjustPosition($range); + } + + $this->context->setPosition($range); + } + + array_push(self::$previousContexts, $this->context); + + return $this->context; + } + + public function initNewContext(): ?AbstractContext + { + return new VariableContext; + } +} diff --git a/composer.json b/composer.json index 17dc4dc..142b14e 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "illuminate/log": "^11.5", "laravel-zero/framework": "^11.0.0", "microsoft/tolerant-php-parser": "^0.1.2", + "phpstan/phpdoc-parser": "^2.3", "stillat/blade-parser": "^1.10" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 2118fe1..4d1c54d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f17bbd5e400471f42d8477b9c18eb8d3", + "content-hash": "26e8b6111294cc63899dee62d8623a9d", "packages": [ { "name": "brick/math", @@ -3154,6 +3154,53 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + }, + "time": "2025-08-30T15:50:23+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -7465,53 +7512,6 @@ }, "time": "2024-11-09T15:12:26+00:00" }, - { - "name": "phpstan/phpdoc-parser", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" - }, - "time": "2024-10-13T11:29:49+00:00" - }, { "name": "phpunit/php-code-coverage", "version": "10.1.16",