diff --git a/php-templates/LICENSE.md b/php-templates/LICENSE.md new file mode 100644 index 0000000..79810c8 --- /dev/null +++ b/php-templates/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Taylor Otwell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/php-templates/README.md b/php-templates/README.md new file mode 100644 index 0000000..40f0e54 --- /dev/null +++ b/php-templates/README.md @@ -0,0 +1,5 @@ +The templates here are based on the official VS Code extension by Laravel https://github.com/laravel/vs-code-extension + +Modifications: + - return instead of echo + - do not serialize to JSON diff --git a/php-templates/configs.php b/php-templates/configs.php new file mode 100644 index 0000000..de7f4d9 --- /dev/null +++ b/php-templates/configs.php @@ -0,0 +1,130 @@ +merge(glob(config_path("**/*.php"))) + ->map(fn ($path) => [ + (string) \Illuminate\Support\Str::of($path) + ->replace([config_path("/"), ".php"], "") + ->replace("/", "."), + $path + ]); + +$vendor = collect(glob(base_path("vendor/**/**/config/*.php")))->map(fn ( + $path +) => [ + (string) \Illuminate\Support\Str::of($path) + ->afterLast("/config/") + ->replace(".php", "") + ->replace("/", "."), + $path +]); + +$configPaths = $local + ->merge($vendor) + ->groupBy(0) + ->map(fn ($items)=>$items->pluck(1)); + +$cachedContents = []; +$cachedParsed = []; + +function vsCodeGetConfigValue($value, $key, $configPaths) { + $parts = explode(".", $key); + $toFind = $key; + $found = null; + + while (count($parts) > 0) { + array_pop($parts); + $toFind = implode(".", $parts); + + if ($configPaths->has($toFind)) { + $found = $toFind; + break; + } + } + + if ($found === null) { + return null; + } + + $file = null; + $line = null; + + if ($found === $key) { + $file = $configPaths->get($found)[0]; + } else { + foreach ($configPaths->get($found) as $path) { + $cachedContents[$path] ??= file_get_contents($path); + $cachedParsed[$path] ??= token_get_all($cachedContents[$path]); + + $keysToFind = \Illuminate\Support\Str::of($key) + ->replaceFirst($found, "") + ->ltrim(".") + ->explode("."); + + if (is_numeric($keysToFind->last())) { + $index = $keysToFind->pop(); + + if ($index !== "0") { + return null; + } + + $key = collect(explode(".", $key)); + $key->pop(); + $key = $key->implode("."); + $value = "array(...)"; + } + + $nextKey = $keysToFind->shift(); + $expectedDepth = 1; + + $depth = 0; + + foreach ($cachedParsed[$path] as $token) { + if ($token === "[") { + $depth++; + } + + if ($token === "]") { + $depth--; + } + + if (!is_array($token)) { + continue; + } + + $str = trim($token[1], '"\''); + + if ( + $str === $nextKey && + $depth === $expectedDepth && + $token[0] === T_CONSTANT_ENCAPSED_STRING + ) { + $nextKey = $keysToFind->shift(); + $expectedDepth++; + + if ($nextKey === null) { + $file = $path; + $line = $token[2]; + break; + } + } + } + + if ($file) { + break; + } + } + } + + return [ + "name" => $key, + "value" => $value, + "file" => $file === null ? null : str_replace(base_path('/'), '', $file), + "line" => $line + ]; +} + +return collect(\Illuminate\Support\Arr::dot(config()->all())) + ->map(fn ($value, $key) => vsCodeGetConfigValue($value, $key, $configPaths)) + ->filter() + ->values(); diff --git a/php-templates/routes.php b/php-templates/routes.php new file mode 100644 index 0000000..bb4a1d0 --- /dev/null +++ b/php-templates/routes.php @@ -0,0 +1,46 @@ +getActionName() === 'Closure') { + return new \ReflectionFunction($route->getAction()['uses']); + } + + if (!str_contains($route->getActionName(), '@')) { + return new \ReflectionClass($route->getActionName()); + } + + try { + return new \ReflectionMethod($route->getControllerClass(), $route->getActionMethod()); + } catch (\Throwable $e) { + $namespace = app(\Illuminate\Routing\UrlGenerator::class)->getRootControllerNamespace() + ?? (app()->getNamespace() . 'Http\Controllers'); + + return new \ReflectionMethod( + $namespace . '\\' . ltrim($route->getControllerClass(), '\\'), + $route->getActionMethod(), + ); + } +} + +return collect(app('router')->getRoutes()->getRoutes()) + ->map(function (\Illuminate\Routing\Route $route) { + try { + $reflection = vsCodeGetRouterReflection($route); + } catch (\Throwable $e) { + $reflection = null; + } + + return [ + 'method' => collect($route->methods())->filter(function ($method) { + return $method !== 'HEAD'; + })->implode('|'), + 'uri' => $route->uri(), + 'name' => $route->getName(), + 'action' => $route->getActionName(), + 'parameters' => $route->parameterNames(), + 'filename' => $reflection ? $reflection->getFileName() : null, + 'line' => $reflection ? $reflection->getStartLine() : null, + ]; + }) + ; diff --git a/php-templates/views.php b/php-templates/views.php new file mode 100644 index 0000000..36a9ad1 --- /dev/null +++ b/php-templates/views.php @@ -0,0 +1,62 @@ +files() + ->name("*.blade.php") + ->in($path) + as $file + ) { + $paths[] = [ + "path" => str_replace(base_path(DIRECTORY_SEPARATOR), '', $file->getRealPath()), + "isVendor" => str_contains($file->getRealPath(), base_path("vendor")), + "key" => \Illuminate\Support\Str::of($file->getRealPath()) + ->replace(realpath($path), "") + ->replace(".blade.php", "") + ->ltrim(DIRECTORY_SEPARATOR) + ->replace(DIRECTORY_SEPARATOR, ".") + ]; + } + + return $paths; +} +$paths = collect( + app("view") + ->getFinder() + ->getPaths() +)->flatMap(function ($path) { + return vsCodeFindBladeFiles($path); +}); + +$hints = collect( + app("view") + ->getFinder() + ->getHints() +)->flatMap(function ($paths, $key) { + return collect($paths)->flatMap(function ($path) use ($key) { + return collect(vsCodeFindBladeFiles($path))->map(function ($value) use ( + $key + ) { + return array_merge($value, ["key" => "{$key}::{$value["key"]}"]); + }); + }); +}); + +[$local, $vendor] = $paths + ->merge($hints) + ->values() + ->partition(function ($v) { + return !$v["isVendor"]; + }); + +return $local + ->sortBy("key", SORT_NATURAL) + ->merge($vendor->sortBy("key", SORT_NATURAL)); diff --git a/resources/views/meta.php b/resources/views/meta.php index f4ba527..38fb1bb 100644 --- a/resources/views/meta.php +++ b/resources/views/meta.php @@ -21,6 +21,14 @@ namespace PHPSTORM_META { ])); + + override(, map([ + $value) : ?> + '' => '', + + ])); + + override(\factory(0), map([ '' => '@FactoryBuilder', @@ -65,14 +73,20 @@ namespace PHPSTORM_META { override(\tap(0), type(0)); override(\optional(0), type(0)); + + $argumentsList) : ?> + registerArgumentsSet('', $arg) : ?>,); + + + $arguments) : ?> - $argumentList) : ?> -expectedArguments(\(), , $arg) : ?>, -); + $argumentSet) : ?> +expectedArguments(, , argumentsSet('')); + } diff --git a/src/Console/MetaCommand.php b/src/Console/MetaCommand.php index 2e0fe1e..df6f2b0 100644 --- a/src/Console/MetaCommand.php +++ b/src/Console/MetaCommand.php @@ -16,6 +16,7 @@ use Illuminate\Console\Command; use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\View\Factory; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Collection; use RuntimeException; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -64,6 +65,16 @@ class MetaCommand extends Command '\Psr\Container\ContainerInterface::get(0)', ]; + protected $configMethods = [ + '\config()', + '\Illuminate\Config\Repository::get()', + '\Illuminate\Config\Repository::set()', + '\Illuminate\Support\Facades\Config::get()', + '\Illuminate\Support\Facades\Config::set()', + ]; + + protected $templateCache = []; + /** * * @param Filesystem $files @@ -120,10 +131,17 @@ class MetaCommand extends Command $this->unregisterClassAutoloadExceptions($ourAutoloader); + $configValues = $this->loadTemplate('configs')->pluck('value', 'name')->map(function ($value, $key) { + return gettype($value); + }); + $content = $this->view->make('meta', [ 'bindings' => $bindings, 'methods' => $this->methods, 'factories' => $factories, + 'configMethods' => $this->configMethods, + 'configValues' => $configValues, + 'expectedArgumentSets' => $this->getExpectedArgumentSets(), 'expectedArguments' => $this->getExpectedArguments(), ])->render(); @@ -168,18 +186,49 @@ class MetaCommand extends Command return $autoloader; } + protected function getExpectedArgumentSets() + { + return [ + 'configs' => $this->loadTemplate('configs')->pluck('name')->filter(), + 'routes' => $this->loadTemplate('routes')->pluck('name')->filter(), + ]; + } + protected function getExpectedArguments() { return [ - 'config' => [ - 0 => $this->getConfigHints(), + '\config()' => [ + 0 => 'configs', + ], + '\Illuminate\Config\Repository::get()' => [ + 0 => 'configs', + ], + '\Illuminate\Config\Repository::set()' => [ + 0 => 'configs', + ], + '\Illuminate\Support\Facades\Config::get()' => [ + 0 => 'configs', + ], + '\Illuminate\Support\Facades\Config::set()' => [ + 0 => 'configs', + ], + '\route()' => [ + 0 => 'routes', ], ]; } - protected function getConfigHints() + /** + * @return Collection + */ + protected function loadTemplate($name) { - return collect($this->config->all())->dot()->keys()->toArray(); + if (!isset($this->templates[$name])) { + $file = __DIR__ . '/../../php-templates/' . basename($name) .'.php'; + $this->templates[$name] = $this->files->requireOnce($file); + } + + return $this->templates[$name]; } /**