├── .editorconfig ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── Commands │ ├── AutocompleteCommand.php │ ├── CompileBinaryCommand.php │ ├── ContextTypeScriptGeneratorCommand.php │ ├── DetectCommand.php │ ├── ResolvesCode.php │ └── Tag.php ├── Contexts │ ├── AbstractContext.php │ ├── Argument.php │ ├── Arguments.php │ ├── ArrayItem.php │ ├── ArrayValue.php │ ├── Assignment.php │ ├── AssignmentValue.php │ ├── Base.php │ ├── Binary.php │ ├── Blade.php │ ├── ClassDefinition.php │ ├── ClosureValue.php │ ├── Contracts │ │ └── HasParameters.php │ ├── MethodCall.php │ ├── MethodDefinition.php │ ├── ObjectValue.php │ ├── Parameter.php │ ├── ParameterValue.php │ ├── Parameters.php │ └── StringValue.php ├── Parser │ ├── Context.php │ ├── DetectContext.php │ ├── DetectWalker.php │ ├── DetectedItem.php │ ├── Parse.php │ ├── Settings.php │ └── Walker.php ├── Parsers │ ├── AbstractParser.php │ ├── AnonymousFunctionCreationExpressionParser.php │ ├── ArgumentExpressionListParser.php │ ├── ArgumentExpressionParser.php │ ├── ArrayCreationExpressionParser.php │ ├── ArrayElementParser.php │ ├── ArrowFunctionCreationExpressionParser.php │ ├── AssignmentExpressionParser.php │ ├── BinaryExpressionParser.php │ ├── CallExpressionParser.php │ ├── ClassBaseClauseParser.php │ ├── ClassDeclarationParser.php │ ├── ClassInterfaceClauseParser.php │ ├── CompoundStatementNodeParser.php │ ├── ExpressionStatementParser.php │ ├── FunctionDeclarationParser.php │ ├── InitsNewContext.php │ ├── InlineHtmlParser.php │ ├── MemberAccessExpressionParser.php │ ├── MethodDeclarationParser.php │ ├── ObjectCreationExpressionParser.php │ ├── ParameterDeclarationListParser.php │ ├── ParameterParser.php │ ├── PropertyDeclarationParser.php │ ├── ReturnStatementParser.php │ ├── ScopedPropertyAccessExpressionParser.php │ ├── SourceFileNodeParser.php │ └── StringLiteralParser.php ├── Providers │ └── AppServiceProvider.php └── Support │ └── Debugs.php ├── bin └── .gitkeep ├── bootstrap ├── app.php └── providers.php ├── box.json ├── composer.json ├── composer.lock ├── config ├── app.php ├── commands.php └── logging.php ├── fix-class-resolutions.php ├── php-parser ├── phpunit.xml.dist ├── pint.json ├── snippets ├── anon-func.php ├── anonymous-function-param.php ├── array-with-arrow-function-missing-second-key.php ├── array-with-arrow-function-several-keys-and-second-param.php ├── array-with-arrow-function-several-keys.php ├── array-with-arrow-function.php ├── arrow-function-param.php ├── basic-function-with-param.php ├── basic-function.php ├── basic-method-with-params.php ├── basic-method.php ├── basic-static-method-with-params.php ├── basic-static-method.php ├── chain-in-method-def.php ├── chained-method-with-params.php ├── chained-static-method-with-params.php ├── chained-static-method.php ├── class-chain-in-method-def.php ├── class-def.php ├── class-in-array-value.php ├── function-def.php ├── nested.php ├── no-parse-closed-string.php ├── object-creation.php ├── property-chain-in-method-def.php ├── route-view.php ├── route.php ├── scratch.php ├── var-chain-in-method-def.php ├── wild.php └── with-array-funcs.php └── tests ├── CreatesApplication.php ├── Pest.php ├── TestCase.php └── Unit ├── DetectTest.php ├── ExampleTest.php └── ParserTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | /.github export-ignore 3 | .scrutinizer.yml export-ignore 4 | BACKERS.md export-ignore 5 | CONTRIBUTING.md export-ignore 6 | CHANGELOG.md export-ignore 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | /.vscode 4 | /.vagrant 5 | .phpunit.result.cache 6 | /buildroot 7 | /downloads 8 | /source 9 | .DS_Store 10 | todo.md 11 | .env 12 | /storage/local-results 13 | /tests/snippets 14 | /storage/new-parsed 15 | storage/tree.txt 16 | /storage/logs 17 | /bin/logs 18 | builds/php-parser 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Parser CLI 2 | -------------------------------------------------------------------------------- /app/Commands/AutocompleteCommand.php: -------------------------------------------------------------------------------- 1 | resolveCode('autocomplete'); 20 | 21 | $walker = new Walker($code, (bool) $this->option('debug')); 22 | $result = $walker->walk(); 23 | 24 | $autocompleting = $result->findAutocompleting(); 25 | 26 | if (app()->isLocal()) { 27 | $this->log($autocompleting, $result, $code); 28 | } 29 | 30 | echo json_encode($autocompleting?->flip() ?? [], $this->option('debug') ? JSON_PRETTY_PRINT : 0); 31 | } 32 | 33 | protected function log($autocompleting, $result, $code) 34 | { 35 | $dir = 'local-results/autocomplete'; 36 | File::ensureDirectoryExists(storage_path($dir)); 37 | $now = now()->format('Y-m-d-H-i-s'); 38 | 39 | if (!$this->option('local-file')) { 40 | File::put(storage_path("{$dir}/{$now}-01-code.php"), $code); 41 | } 42 | 43 | File::put(storage_path("{$dir}/{$now}-02-autocomplete.json"), json_encode($autocompleting?->flip() ?? [], JSON_PRETTY_PRINT)); 44 | File::put(storage_path("{$dir}/{$now}-03-full.json"), $result->toJson(JSON_PRETTY_PRINT)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Commands/CompileBinaryCommand.php: -------------------------------------------------------------------------------- 1 | option('arch')) 31 | ); 32 | 33 | info("Destination: {$destination}"); 34 | 35 | if (file_exists(base_path('.env'))) { 36 | exec('mv ' . base_path('.env') . ' ' . base_path('.env.bak')); 37 | } 38 | 39 | file_put_contents(base_path('.env'), ''); 40 | 41 | exec('composer install --no-dev'); 42 | 43 | $extensions = collect([ 44 | 'bcmath', 45 | 'calendar', 46 | 'ctype', 47 | 'curl', 48 | 'dba', 49 | 'dom', 50 | 'exif', 51 | 'fileinfo', 52 | 'filter', 53 | 'iconv', 54 | 'mbregex', 55 | 'mbstring', 56 | 'openssl', 57 | 'pcntl', 58 | 'pdo_mysql', 59 | 'pdo_sqlite', 60 | 'pdo', 61 | 'phar', 62 | 'posix', 63 | 'readline', 64 | 'session', 65 | 'simplexml', 66 | 'sockets', 67 | 'sodium', 68 | 'sqlite3', 69 | 'tokenizer', 70 | 'xml', 71 | 'xmlreader', 72 | 'xmlwriter', 73 | 'zip', 74 | 'zlib', 75 | ])->implode(','); 76 | 77 | $spc = base_path('spc'); 78 | 79 | collect([ 80 | base_path('php-parser') . " app:build --build-version={$version}", 81 | sprintf('%s download --with-php=8.2 --for-extensions="%s" --prefer-pre-built', $spc, $extensions), 82 | sprintf('%s doctor --auto-fix', $spc), 83 | sprintf('%s build --build-micro "%s"', $spc, $extensions), 84 | sprintf('%s micro:combine %s -O %s', $spc, base_path('builds/php-parser'), $destination), 85 | ])->each(function (string $command) use ($timeout) { 86 | Process::timeout($timeout)->run($command, function (string $type, string $output) { 87 | echo $output; 88 | }); 89 | }); 90 | 91 | if (!file_exists($destination)) { 92 | throw new \Exception('Error during compilation'); 93 | } 94 | 95 | if (file_exists(base_path('.env.bak'))) { 96 | exec('mv ' . base_path('.env.bak') . ' ' . base_path('.env')); 97 | } 98 | 99 | info("Binary compiled successfully at {$destination}"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/Commands/ContextTypeScriptGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | line('namespace AutocompleteParsingResult {'); 16 | 17 | $classes = collect(glob(base_path('app/Contexts/*.php'))) 18 | ->filter(fn ($file) => !str_contains($file, 'Abstract')); 19 | 20 | $this->line('type Result = ' . $classes->map(fn ($file) => pathinfo($file, PATHINFO_FILENAME))->join(' | ') . ';'); 21 | 22 | $this->newLine(); 23 | 24 | $classes->each(function ($file) { 25 | $className = pathinfo($file, PATHINFO_FILENAME); 26 | $namespace = 'App\\Contexts\\' . $className; 27 | 28 | $this->line("export interface {$className} {"); 29 | 30 | $inst = new $namespace; 31 | 32 | $reflection = new \ReflectionClass($inst); 33 | 34 | $this->line("type: '{$inst->type()}';"); 35 | $this->line('parent: Result | null;'); 36 | 37 | if ($reflection->getProperty('hasChildren')->getValue($inst)) { 38 | $this->line('children: Result[];'); 39 | } 40 | 41 | $properties = collect($reflection->getProperties(\ReflectionProperty::IS_PUBLIC)) 42 | ->filter(fn ($prop) => !in_array($prop->getName(), [ 43 | 'children', 44 | 'autocompleting', 45 | 'freshObject', 46 | 'hasChildren', 47 | 'parent', 48 | 'label', 49 | ])) 50 | ->map(fn ($prop) => [ 51 | 'name' => $prop->getName(), 52 | 'type' => str_replace('App\Contexts\\', '', $prop->getType()?->getName() ?? 'any'), 53 | 'default' => $prop->getValue($inst), 54 | ]) 55 | ->each(function ($prop) { 56 | $addon = ($prop['default'] === null) ? ' | null' : ''; 57 | $this->line("{$prop['name']}: {$prop['type']}{$addon};"); 58 | }); 59 | 60 | $this->line('}'); 61 | $this->newLine(); 62 | }); 63 | 64 | $this->line('}'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Commands/DetectCommand.php: -------------------------------------------------------------------------------- 1 | resolveCode('detect'); 20 | 21 | $walker = new DetectWalker($code, (bool) $this->option('debug')); 22 | $result = $walker->walk(); 23 | 24 | if (app()->isLocal()) { 25 | $this->log($result, $code); 26 | } 27 | 28 | echo $result->toJson($this->option('debug') ? JSON_PRETTY_PRINT : 0); 29 | } 30 | 31 | protected function log($result, $code) 32 | { 33 | $dir = 'local-results/detect'; 34 | File::ensureDirectoryExists(storage_path($dir)); 35 | $now = now()->format('Y-m-d-H-i-s'); 36 | 37 | File::put(storage_path("{$dir}/result-{$now}.json"), $result->toJson(JSON_PRETTY_PRINT)); 38 | 39 | if (!$this->option('local-file')) { 40 | File::put(storage_path("{$dir}/result-{$now}.php"), $code); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Commands/ResolvesCode.php: -------------------------------------------------------------------------------- 1 | option('local-file')) { 10 | return file_get_contents(__DIR__ . '/../../tests/snippets/' . $path . '/' . $this->option('from-file') . '.php'); 11 | } 12 | 13 | $code = $this->argument('code'); 14 | 15 | if ($this->option('from-file')) { 16 | return file_get_contents($code); 17 | } 18 | 19 | return $code; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Commands/Tag.php: -------------------------------------------------------------------------------- 1 | freshObject = $this->freshArray(); 40 | } 41 | 42 | public function flip() 43 | { 44 | return array_merge( 45 | Arr::except($this->toArray(), ['children']), 46 | ['parent' => $this->parent?->flip()], 47 | ); 48 | } 49 | 50 | public function findAutocompleting(?AbstractContext $context = null) 51 | { 52 | $context = $context ?? $this; 53 | $result = $this->searchForAutocompleting($context, true); 54 | $lastResult = null; 55 | 56 | while ($result !== null) { 57 | $lastResult = $result; 58 | $result = $this->searchForAutocompleting($result); 59 | } 60 | 61 | return $lastResult; 62 | } 63 | 64 | protected function searchForAutocompleting(AbstractContext $context, $checkCurrent = false) 65 | { 66 | if ($checkCurrent && $context->autocompleting && $context->isAbleToAutocomplete) { 67 | return $context; 68 | } 69 | 70 | $publicProps = Arr::except(get_object_vars($context), ['freshObject', 'parent']); 71 | 72 | foreach ($publicProps as $child) { 73 | $child = is_array($child) ? $child : [$child]; 74 | 75 | foreach ($child as $subChild) { 76 | if ($subChild instanceof AbstractContext) { 77 | $result = $this->findAutocompleting($subChild); 78 | 79 | if ($result) { 80 | return $result; 81 | } 82 | } 83 | } 84 | } 85 | 86 | return null; 87 | } 88 | 89 | protected function freshArray() 90 | { 91 | return $this->toArray(); 92 | } 93 | 94 | public function initNew(AbstractContext $newContext) 95 | { 96 | $newContext->parent = $this; 97 | 98 | $this->children[] = $newContext; 99 | 100 | return $newContext; 101 | } 102 | 103 | public function searchForVar(?string $name): AssignmentValue|string|null 104 | { 105 | if ($name === null) { 106 | return null; 107 | } 108 | 109 | if (property_exists($this, 'parameters') && $this->parameters instanceof Parameters) { 110 | foreach ($this->parameters->children as $param) { 111 | if ($param->name === $name) { 112 | return $param->types[0] ?? null; 113 | } 114 | } 115 | } 116 | 117 | foreach ($this->children as $child) { 118 | if ($child instanceof Assignment && $child->name === $name) { 119 | return $child->value; 120 | } 121 | } 122 | 123 | return $this->parent?->searchForVar($name) ?? null; 124 | } 125 | 126 | public function addPropertyToNearestClassDefinition(?string $name, $types = []) 127 | { 128 | if ($name === null) { 129 | return; 130 | } 131 | 132 | if ($this instanceof ClassDefinition) { 133 | $this->properties[] = [ 134 | 'name' => $name, 135 | 'types' => $types, 136 | ]; 137 | } else { 138 | $this->parent?->addPropertyToNearestClassDefinition($name, $types); 139 | } 140 | } 141 | 142 | public function nearestClassDefinition() 143 | { 144 | if ($this instanceof ClassDefinition) { 145 | return $this; 146 | } 147 | 148 | return $this->parent?->nearestClassDefinition() ?? null; 149 | } 150 | 151 | public function searchForProperty(string $name) 152 | { 153 | if ($this instanceof ClassDefinition) { 154 | return collect($this->properties)->first(fn ($prop) => $prop['name'] === $name); 155 | } 156 | 157 | return $this->parent?->searchForProperty($name) ?? null; 158 | } 159 | 160 | public function pristine(): bool 161 | { 162 | return $this->freshObject === $this->freshArray(); 163 | } 164 | 165 | public function touched(): bool 166 | { 167 | return !$this->pristine(); 168 | } 169 | 170 | public function toArray(): array 171 | { 172 | return array_merge( 173 | ['type' => $this->type()], 174 | $this->autocompleting ? ['autocompleting' => true] : [], 175 | $this->castToArray(), 176 | ($this->label !== '') ? ['label' => $this->label] : [], 177 | (count($this->start) > 0) ? ['start' => $this->start] : [], 178 | (count($this->end) > 0) ? ['end' => $this->end] : [], 179 | ($this->hasChildren) 180 | ? ['children' => array_map(fn ($child) => $child->toArray(), $this->children)] 181 | : [], 182 | ); 183 | } 184 | 185 | public function toJson($flags = 0) 186 | { 187 | return json_encode($this->toArray(), $flags); 188 | } 189 | 190 | public function setPosition(Range $range) 191 | { 192 | $this->start = [ 193 | 'line' => $range->start->line, 194 | 'column' => $range->start->character, 195 | ]; 196 | 197 | $this->end = [ 198 | 'line' => $range->end->line, 199 | 'column' => $range->end->character, 200 | ]; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /app/Contexts/Argument.php: -------------------------------------------------------------------------------- 1 | $this->name, 18 | ]; 19 | } 20 | 21 | public function isAutoCompleting(): bool 22 | { 23 | if ($this->autocompleting) { 24 | return true; 25 | } 26 | 27 | return collect($this->children)->first( 28 | fn ($child) => $child->autocompleting 29 | ) !== null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Contexts/Arguments.php: -------------------------------------------------------------------------------- 1 | children)->search( 15 | fn ($child) => method_exists($child, 'isAutoCompleting') ? $child->isAutoCompleting() : false, 16 | ); 17 | 18 | if ($autocompletingIndex === false) { 19 | $autocompletingIndex = count($this->children); 20 | } 21 | 22 | return [ 23 | 'autocompletingIndex' => $autocompletingIndex, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Contexts/ArrayItem.php: -------------------------------------------------------------------------------- 1 | $this->getKey()?->toArray(), 27 | 'value' => $this->getValue()?->toArray(), 28 | 29 | ] + $this->getAutoCompletingValueData(); 30 | } 31 | 32 | protected function getAutoCompletingValueData(): array 33 | { 34 | if ($this->autocompletingValue || $this->hasAutoCompletingChild($this)) { 35 | return ['autocompletingValue' => true]; 36 | } 37 | 38 | return []; 39 | } 40 | 41 | protected function hasAutoCompletingChild(AbstractContext $context): bool 42 | { 43 | foreach ($context->children as $child) { 44 | if ($child->autocompleting || $this->hasAutoCompletingChild($child)) { 45 | return true; 46 | } 47 | } 48 | 49 | return false; 50 | } 51 | 52 | protected function getKey(): ?AbstractContext 53 | { 54 | if (count($this->children) === 1 && $this->hasKey) { 55 | return $this->children[0]; 56 | } 57 | 58 | if (count($this->children) === 2) { 59 | return $this->children[0]; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | protected function getValue(): ?AbstractContext 66 | { 67 | if (count($this->children) === 1 && !$this->hasKey) { 68 | return $this->children[0]; 69 | } 70 | 71 | if (count($this->children) === 2) { 72 | return $this->children[1]; 73 | } 74 | 75 | return null; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/Contexts/ArrayValue.php: -------------------------------------------------------------------------------- 1 | extraData()); 15 | } 16 | 17 | protected function extraData(): array 18 | { 19 | if (!$this->autocompleting) { 20 | return []; 21 | } 22 | 23 | if (count($this->children) === 0) { 24 | return [ 25 | 'autocompletingKey' => true, 26 | 'autocompletingValue' => true, 27 | ]; 28 | } 29 | 30 | $valueToAutocomplete = collect($this->children)->first( 31 | fn ($child) => $child->toArray()['autocompletingValue'] ?? false, 32 | ); 33 | 34 | if ($valueToAutocomplete) { 35 | return [ 36 | 'autocompletingKey' => false, 37 | 'autocompletingValue' => true, 38 | ]; 39 | } 40 | 41 | $firstChild = $this->children[0]; 42 | 43 | return [ 44 | 'autocompletingKey' => $firstChild->hasKey, 45 | 'autocompletingValue' => !$firstChild->hasKey, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Contexts/Assignment.php: -------------------------------------------------------------------------------- 1 | value = new AssignmentValue; 16 | $this->value->parent = $this; 17 | } 18 | 19 | public function type(): string 20 | { 21 | return 'assignment'; 22 | } 23 | 24 | public function castToArray(): array 25 | { 26 | return [ 27 | 'name' => $this->name, 28 | 'value' => $this->value->toArray(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Contexts/AssignmentValue.php: -------------------------------------------------------------------------------- 1 | children[0] ?? null; 20 | 21 | if (!$child) { 22 | return null; 23 | } 24 | 25 | return [ 26 | 'name' => $child->name ?? $child->className ?? null, 27 | 'type' => $child->type(), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Contexts/Base.php: -------------------------------------------------------------------------------- 1 | $this->className, 24 | 'extends' => $this->extends, 25 | 'implements' => $this->implements, 26 | 'properties' => $this->properties, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Contexts/ClosureValue.php: -------------------------------------------------------------------------------- 1 | parameters = new Parameters; 14 | $this->parameters->parent = $this; 15 | } 16 | 17 | public function type(): string 18 | { 19 | return 'closure'; 20 | } 21 | 22 | public function castToArray(): array 23 | { 24 | return [ 25 | 'parameters' => $this->parameters->toArray(), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Contexts/Contracts/HasParameters.php: -------------------------------------------------------------------------------- 1 | arguments = new Arguments; 18 | $this->arguments->parent = $this; 19 | } 20 | 21 | public function type(): string 22 | { 23 | return 'methodCall'; 24 | } 25 | 26 | public function castToArray(): array 27 | { 28 | return [ 29 | 'methodName' => $this->methodName, 30 | 'className' => $this->className, 31 | 'arguments' => $this->arguments->toArray(), 32 | ]; 33 | } 34 | 35 | public function name() 36 | { 37 | return $this->methodName; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Contexts/MethodDefinition.php: -------------------------------------------------------------------------------- 1 | parameters = new Parameters; 16 | $this->parameters->parent = $this; 17 | } 18 | 19 | public function type(): string 20 | { 21 | return 'methodDefinition'; 22 | } 23 | 24 | public function castToArray(): array 25 | { 26 | return [ 27 | 'methodName' => $this->methodName, 28 | 'parameters' => $this->parameters->toArray(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Contexts/ObjectValue.php: -------------------------------------------------------------------------------- 1 | arguments = new Arguments; 16 | $this->arguments->parent = $this; 17 | } 18 | 19 | public function type(): string 20 | { 21 | return 'object'; 22 | } 23 | 24 | public function castToArray(): array 25 | { 26 | return [ 27 | 'className' => $this->className, 28 | 'arguments' => $this->arguments->toArray(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Contexts/Parameter.php: -------------------------------------------------------------------------------- 1 | value = new ParameterValue; 20 | $this->value->parent = $this; 21 | } 22 | 23 | public function type(): string 24 | { 25 | return 'parameter'; 26 | } 27 | 28 | public function castToArray(): array 29 | { 30 | return [ 31 | 'types' => $this->types, 32 | 'name' => $this->name, 33 | // 'value' => $this->value->toArray(), 34 | ]; 35 | } 36 | 37 | public function toArray(): array 38 | { 39 | return Arr::except(parent::toArray(), ['type']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Contexts/ParameterValue.php: -------------------------------------------------------------------------------- 1 | children[0] ?? null; 20 | 21 | if ($child) { 22 | return [ 23 | 'name' => $child->name(), 24 | 'type' => $child->type(), 25 | ]; 26 | } 27 | 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Contexts/Parameters.php: -------------------------------------------------------------------------------- 1 | $this->value, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Parser/Context.php: -------------------------------------------------------------------------------- 1 | freshObject = $this->freshArray(); 44 | } 45 | 46 | protected function freshArray() 47 | { 48 | return Arr::except($this->toArray(), ['parent']); 49 | } 50 | 51 | public function initNew() 52 | { 53 | if ($this->pristine()) { 54 | return $this; 55 | } 56 | 57 | $newContext = new static; 58 | 59 | $this->children[] = $newContext; 60 | 61 | return $newContext; 62 | } 63 | 64 | public function pristine(): bool 65 | { 66 | return $this->freshObject === $this->freshArray(); 67 | } 68 | 69 | public function touched(): bool 70 | { 71 | return !$this->pristine(); 72 | } 73 | 74 | public function addVariable(string $name, array $attributes) 75 | { 76 | if (isset($attributes['name'])) { 77 | unset($attributes['name']); 78 | } 79 | 80 | $this->variables[ltrim($name, '$')] = $attributes; 81 | } 82 | 83 | public function searchForProperty(string $name) 84 | { 85 | $prop = $this->definedProperties[$name]; 86 | 87 | if ($prop) { 88 | return $prop; 89 | } 90 | 91 | if ($this->parent) { 92 | return $this->parent->searchForProperty($name); 93 | } 94 | 95 | return null; 96 | } 97 | 98 | public function searchForVar(string $name) 99 | { 100 | $param = array_filter( 101 | $this->methodDefinitionParams, 102 | fn ($param) => $param['name'] === $name, 103 | ); 104 | 105 | if (count($param)) { 106 | return array_values($param)[0]; 107 | } 108 | 109 | if (array_key_exists($name, $this->variables)) { 110 | return $this->variables[$name]; 111 | } 112 | 113 | if ($this->parent) { 114 | return $this->parent->searchForVar($name); 115 | } 116 | 117 | return null; 118 | } 119 | 120 | public function toArray() 121 | { 122 | return [ 123 | 'classDefinition' => $this->classDefinition, 124 | 'implements' => $this->implements, 125 | 'extends' => $this->extends, 126 | 'methodDefinition' => $this->methodDefinition, 127 | 'methodDefinitionParams' => $this->methodDefinitionParams, 128 | 'methodExistingArgs' => $this->methodExistingArgs, 129 | 'classUsed' => $this->classUsed, 130 | 'methodUsed' => $this->methodUsed, 131 | 'parent' => $this->parent?->toArray(), 132 | 'variables' => $this->variables, 133 | 'definedProperties' => $this->definedProperties, 134 | 'fillingInArrayKey' => $this->fillingInArrayKey, 135 | 'fillingInArrayValue' => $this->fillingInArrayValue, 136 | 'paramIndex' => $this->paramIndex, 137 | 'children' => array_map(fn ($child) => $child->toArray(), $this->children), 138 | ]; 139 | } 140 | 141 | public function toJson($flags = 0) 142 | { 143 | return json_encode($this->toArray(), $flags); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/Parser/DetectContext.php: -------------------------------------------------------------------------------- 1 | freshObject = $this->toArray(); 40 | } 41 | 42 | public function pristine(): bool 43 | { 44 | return $this->toArray() === $this->freshObject; 45 | } 46 | 47 | public function touched(): bool 48 | { 49 | return !$this->pristine(); 50 | } 51 | 52 | public function addVariable(string $name, array $attributes) 53 | { 54 | if (isset($attributes['name'])) { 55 | unset($attributes['name']); 56 | } 57 | 58 | $this->variables[ltrim($name, '$')] = $attributes; 59 | } 60 | 61 | public function searchForProperty(string $name) 62 | { 63 | $prop = $this->definedProperties[$name]; 64 | 65 | if ($prop) { 66 | return $prop; 67 | } 68 | 69 | if ($this->parent) { 70 | return $this->parent->searchForProperty($name); 71 | } 72 | 73 | return null; 74 | } 75 | 76 | public function searchForVar(string $name) 77 | { 78 | $param = array_filter( 79 | $this->methodDefinitionParams, 80 | fn ($param) => $param['name'] === $name, 81 | ); 82 | 83 | if (count($param)) { 84 | return array_values($param)[0]; 85 | } 86 | 87 | if (array_key_exists($name, $this->variables)) { 88 | return $this->variables[$name]; 89 | } 90 | 91 | if ($this->parent) { 92 | return $this->parent->searchForVar($name); 93 | } 94 | 95 | return null; 96 | } 97 | 98 | public function toArray() 99 | { 100 | return [ 101 | 'classDefinition' => $this->classDefinition, 102 | 'implements' => $this->implements, 103 | 'extends' => $this->extends, 104 | 'methodDefinition' => $this->methodDefinition, 105 | 'methodDefinitionParams' => $this->methodDefinitionParams, 106 | 'methodExistingArgs' => $this->methodExistingArgs, 107 | 'classUsed' => $this->classUsed, 108 | 'methodUsed' => $this->methodUsed, 109 | 'parent' => $this->parent?->toArray(), 110 | 'variables' => $this->variables, 111 | 'definedProperties' => $this->definedProperties, 112 | 'fillingInArrayKey' => $this->fillingInArrayKey, 113 | 'fillingInArrayValue' => $this->fillingInArrayValue, 114 | 'paramIndex' => $this->paramIndex, 115 | ]; 116 | } 117 | 118 | public function toJson($flags = 0) 119 | { 120 | return json_encode($this->toArray(), $flags); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/Parser/DetectWalker.php: -------------------------------------------------------------------------------- 1 | debug = $debug; 34 | $this->sourceFile = (new Parser)->parseSourceFile(trim($this->document)); 35 | $this->context = new DetectContext; 36 | } 37 | 38 | public function walk(?Node $node = null) 39 | { 40 | Settings::$capturePosition = true; 41 | 42 | Parse::$debug = $this->debug; 43 | 44 | Parse::parse( 45 | node: $this->sourceFile, 46 | callback: $this->handleContext(...), 47 | ); 48 | 49 | return collect($this->items)->map(fn ($item) => Arr::except($item->toArray(), 'children')); 50 | } 51 | 52 | protected function handleContext(Node $node, AbstractContext $context) 53 | { 54 | $nodesToDetect = [ 55 | CallExpression::class, 56 | ObjectCreationExpression::class, 57 | ]; 58 | 59 | foreach ($nodesToDetect as $nodeClass) { 60 | if ($node instanceof $nodeClass) { 61 | $this->items[] = $context; 62 | 63 | $context->parent->children = array_filter($context->parent->children, fn ($child) => $child !== $context); 64 | } 65 | } 66 | 67 | if ($context instanceof Blade) { 68 | foreach ($context->children as $child) { 69 | $this->items[] = $child; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Parser/DetectedItem.php: -------------------------------------------------------------------------------- 1 | freshObject = $this->toArray(); 18 | } 19 | 20 | public function pristine(): bool 21 | { 22 | return $this->toArray() === $this->freshObject; 23 | } 24 | 25 | public function touched(): bool 26 | { 27 | return !$this->pristine(); 28 | } 29 | 30 | public function toArray() 31 | { 32 | return [ 33 | 'class' => $this->classUsed, 34 | 'method' => $this->methodUsed, 35 | 'params' => $this->params, 36 | ]; 37 | } 38 | 39 | public function toJson($flags = 0) 40 | { 41 | return json_encode($this->toArray(), $flags); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Parser/Parse.php: -------------------------------------------------------------------------------- 1 | make($parserClass); 35 | $parser->context($context)->depth($depth + 1); 36 | 37 | if ($newContext = $parser->initNewContext()) { 38 | $context = $context->initNew($newContext); 39 | $parser->context($context); 40 | } 41 | 42 | self::debug($depth, '+ Context:', $context::class); 43 | self::debug($depth, '* Parsing: ' . $parserClass); 44 | self::debugBreak(); 45 | 46 | $context = $parser->parseNode($node); 47 | 48 | if ($callback) { 49 | $callback($node, $context); 50 | } 51 | } 52 | 53 | foreach ($node->getChildNodes() as $child) { 54 | self::parse($child, $depth + 1, $context, $callback); 55 | } 56 | 57 | return $context; 58 | } 59 | 60 | public static function tree(Node $node, $depth = 0) 61 | { 62 | echo str_repeat(' ', $depth) . $node::class . ' `' . substr(str_replace("\n", ' ', $node->getText()), 0, 25) . '`' . PHP_EOL; 63 | 64 | foreach ($node->getChildNodes() as $child) { 65 | self::tree($child, $depth + 1); 66 | } 67 | } 68 | 69 | protected static function getCodeSnippet(Node $node) 70 | { 71 | $stripped = preg_replace( 72 | '/\s+/', 73 | ' ', 74 | str_replace("\n", ' ', $node->getText()) 75 | ); 76 | 77 | return substr($stripped, 0, 50); 78 | } 79 | 80 | protected static function debug($depth, ...$messages) 81 | { 82 | if (self::$debug) { 83 | echo str_repeat(' ', $depth) . implode(' ', $messages) . PHP_EOL; 84 | } 85 | } 86 | 87 | protected static function debugBreak() 88 | { 89 | if (self::$debug) { 90 | echo PHP_EOL; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/Parser/Settings.php: -------------------------------------------------------------------------------- 1 | debug = $debug; 29 | $this->sourceFile = (new Parser)->parseSourceFile(trim($this->document)); 30 | $this->context = new Context; 31 | } 32 | 33 | protected function documentSkipsClosingQuote() 34 | { 35 | if (count($this->sourceFile->statementList) === 1 && $this->sourceFile->statementList[0] instanceof InlineHtml) { 36 | // Probably Blade... 37 | $lastChar = substr($this->sourceFile->getFullText(), -1); 38 | $closesWithQuote = in_array($lastChar, ['"', "'"]); 39 | 40 | return $closesWithQuote; 41 | } 42 | 43 | foreach ($this->sourceFile->getDescendantNodesAndTokens() as $child) { 44 | if ($child instanceof SkippedToken && $child->getText($this->sourceFile->getFullText()) === "'") { 45 | return true; 46 | } 47 | } 48 | 49 | return false; 50 | } 51 | 52 | public function walk() 53 | { 54 | if (!$this->documentSkipsClosingQuote()) { 55 | return new Base; 56 | } 57 | 58 | Parse::$debug = $this->debug; 59 | 60 | $parsed = Parse::parse($this->sourceFile); 61 | 62 | return $parsed; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Parsers/AbstractParser.php: -------------------------------------------------------------------------------- 1 | context = $context; 17 | 18 | return $this; 19 | } 20 | 21 | public function parseNode(Node $node): AbstractContext 22 | { 23 | if (method_exists($this, 'parse')) { 24 | return call_user_func([$this, 'parse'], $node); 25 | } 26 | 27 | return $this->context; 28 | } 29 | 30 | public function depth(int $depth) 31 | { 32 | $this->depth = $depth; 33 | 34 | return $this; 35 | } 36 | 37 | public function initNewContext(): ?AbstractContext 38 | { 39 | return null; 40 | } 41 | 42 | public function indent($message) 43 | { 44 | return str_repeat(' ', $this->depth) . $message; 45 | } 46 | 47 | public function loopChildren(): bool 48 | { 49 | return true; 50 | } 51 | 52 | public function debug(...$messages) 53 | { 54 | foreach ($messages as $message) { 55 | echo $this->indent($message) . PHP_EOL; 56 | } 57 | } 58 | 59 | protected function parentNodeIs(Node $node, array $nodeClasses): bool 60 | { 61 | if ($node->getParent() === null) { 62 | return false; 63 | } 64 | 65 | return in_array(get_class($node->getParent()), $nodeClasses) 66 | || $this->parentNodeIs($node->getParent(), $nodeClasses); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/Parsers/AnonymousFunctionCreationExpressionParser.php: -------------------------------------------------------------------------------- 1 | context instanceof MethodCall) { 18 | return $this->context->arguments; 19 | } 20 | 21 | return $this->context; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Parsers/ArgumentExpressionParser.php: -------------------------------------------------------------------------------- 1 | context->name = $node->name?->getText($node->getFileContents()); 19 | 20 | return $this->context; 21 | } 22 | 23 | public function initNewContext(): ?AbstractContext 24 | { 25 | return new Argument; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Parsers/ArrayCreationExpressionParser.php: -------------------------------------------------------------------------------- 1 | parentNodeIs($node, [CallExpression::class, ObjectCreationExpression::class])) { 25 | $this->context->isAbleToAutocomplete = true; 26 | } 27 | 28 | $this->context->autocompleting = $node->closeParenOrBracket instanceof MissingToken; 29 | 30 | return $this->context; 31 | } 32 | 33 | public function initNewContext(): ?AbstractContext 34 | { 35 | return new ArrayValue; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Parsers/ArrayElementParser.php: -------------------------------------------------------------------------------- 1 | context->hasKey = $node->elementKey !== null; 20 | $this->context->autocompletingValue = $node->elementValue instanceof MissingToken; 21 | 22 | return $this->context; 23 | } 24 | 25 | public function initNewContext(): ?AbstractContext 26 | { 27 | return new ArrayItem; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Parsers/ArrowFunctionCreationExpressionParser.php: -------------------------------------------------------------------------------- 1 | context->name = ltrim($node->leftOperand->getText(), '$'); 19 | 20 | return $this->context->value; 21 | } 22 | 23 | public function initNewContext(): ?AbstractContext 24 | { 25 | return new Assignment; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Parsers/BinaryExpressionParser.php: -------------------------------------------------------------------------------- 1 | context; 19 | } 20 | 21 | public function initNewContext(): ?AbstractContext 22 | { 23 | return new Binary; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Parsers/CallExpressionParser.php: -------------------------------------------------------------------------------- 1 | context->methodName) { 21 | return $this->context; 22 | } 23 | 24 | if ($node->callableExpression instanceof QualifiedName) { 25 | $this->context->methodName = (string) ($node->callableExpression->getResolvedName() ?? $node->callableExpression->getText()); 26 | } 27 | 28 | $this->context->autocompleting = $node->closeParen instanceof MissingToken; 29 | 30 | return $this->context; 31 | } 32 | 33 | public function initNewContext(): ?AbstractContext 34 | { 35 | // TODO: Unclear if this is correct 36 | if (!($this->context instanceof MethodCall) || $this->context->touched()) { 37 | return new MethodCall; 38 | } 39 | 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Parsers/ClassBaseClauseParser.php: -------------------------------------------------------------------------------- 1 | context->extends = (string) $node->baseClass->getResolvedName(); 19 | 20 | return $this->context; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Parsers/ClassDeclarationParser.php: -------------------------------------------------------------------------------- 1 | context->className = (string) $node->getNamespacedName(); 19 | 20 | return $this->context; 21 | } 22 | 23 | public function initNewContext(): ?AbstractContext 24 | { 25 | return new ClassDefinition; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Parsers/ClassInterfaceClauseParser.php: -------------------------------------------------------------------------------- 1 | interfaceNameList) { 19 | return $this->context; 20 | } 21 | 22 | foreach ($node->interfaceNameList->getElements() as $element) { 23 | $this->context->implements[] = (string) $element->getResolvedName(); 24 | } 25 | 26 | return $this->context; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Parsers/CompoundStatementNodeParser.php: -------------------------------------------------------------------------------- 1 | context; 19 | } 20 | 21 | public function initNewContext(): ?AbstractContext 22 | { 23 | return null; 24 | 25 | if (!($this->context instanceof MethodCall)) { 26 | return new MethodCall; 27 | } 28 | 29 | return null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Parsers/FunctionDeclarationParser.php: -------------------------------------------------------------------------------- 1 | context->methodName = collect($node->getNameParts())->map( 20 | fn (Token $part) => $part->getText($node->getRoot()->getFullText()) 21 | )->join('\\'); 22 | 23 | return $this->context; 24 | } 25 | 26 | public function initNewContext(): ?AbstractContext 27 | { 28 | return new MethodDefinition; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Parsers/InitsNewContext.php: -------------------------------------------------------------------------------- 1 | '!!}', 24 | '{{{' => '}}}', 25 | '{{' => '}}', 26 | ]; 27 | 28 | protected $startLine = 0; 29 | 30 | /** 31 | * @var Blade 32 | */ 33 | protected AbstractContext $context; 34 | 35 | protected array $items = []; 36 | 37 | /** 38 | * Stillat\BladeParser\Document\Document::fromText treats multibyte characters 39 | * as indentations and spaces resulting in a miscalculated Node position. 40 | * 41 | * This function replaces the multibyte characters with a single, placeholder character 42 | */ 43 | private function replaceMultibyteChars(string $text, string $placeholder = '*'): string 44 | { 45 | return preg_replace('/[^\x00-\x7F]/u', $placeholder, $text); 46 | } 47 | 48 | public function parse(InlineHtml $node) 49 | { 50 | if ($node->getStartPosition() > 0) { 51 | $range = PositionUtilities::getRangeFromPosition( 52 | $node->getStartPosition(), 53 | mb_strlen($node->getText()), 54 | $node->getRoot()->getFullText(), 55 | ); 56 | 57 | $this->startLine = $range->start->line; 58 | } 59 | 60 | $this->parseBladeContent(Document::fromText( 61 | $this->replaceMultibyteChars($node->getText()) 62 | )); 63 | 64 | if (count($this->items)) { 65 | $blade = new Blade; 66 | $this->context->initNew($blade); 67 | 68 | $blade->children = $this->items; 69 | 70 | return $blade; 71 | } 72 | 73 | return $this->context; 74 | } 75 | 76 | protected function parseBladeContent($node) 77 | { 78 | foreach ($node->getNodes() as $child) { 79 | // TODO: Add other echo types as well 80 | if ($child instanceof LiteralNode) { 81 | $this->parseLiteralNode($child); 82 | } 83 | 84 | if ($child instanceof DirectiveNode) { 85 | $this->parseBladeDirective($child); 86 | } 87 | 88 | if ($child instanceof EchoNode) { 89 | $this->parseEchoNode($child); 90 | } 91 | 92 | $this->parseBladeContent($child); 93 | } 94 | } 95 | 96 | protected function doEchoParse(BaseNode $node, $prefix, $content) 97 | { 98 | $snippet = "getStartIndentationLevel()) . str_replace($prefix, '', $content) . ';'; 99 | 100 | $sourceFile = (new Parser)->parseSourceFile($snippet); 101 | 102 | $suffix = $this->echoStrings[$prefix]; 103 | 104 | Settings::$calculatePosition = function (Range $range) use ($node, $prefix, $suffix) { 105 | if ($range->start->line === 1) { 106 | $range->start->character += mb_strlen($prefix); 107 | $range->end->character += mb_strlen($suffix); 108 | } 109 | 110 | $range->start->line += $this->startLine + $node->position->startLine - 2; 111 | $range->end->line += $this->startLine + $node->position->startLine - 2; 112 | 113 | return $range; 114 | }; 115 | 116 | $result = Parse::parse($sourceFile); 117 | 118 | if (count($result->children) === 0) { 119 | return; 120 | } 121 | 122 | $child = $result->children[0]; 123 | 124 | $this->items[] = $child; 125 | } 126 | 127 | protected function parseLiteralNode(LiteralNode $node) 128 | { 129 | foreach ($this->echoStrings as $prefix => $suffix) { 130 | if (!str_starts_with($node->content, $prefix)) { 131 | continue; 132 | } 133 | 134 | $this->doEchoParse($node, $prefix, $node->content); 135 | } 136 | } 137 | 138 | protected function parseBladeDirective(DirectiveNode $node) 139 | { 140 | if ($node->isClosingDirective || !$node->hasArguments()) { 141 | return; 142 | } 143 | 144 | $methodUsed = '@' . $node->content; 145 | $safetyPrefix = 'directive'; 146 | $snippet = "getStartIndentationLevel()) . str_replace($methodUsed, $safetyPrefix . $node->content, $node->toString() . ';'); 147 | 148 | $sourceFile = (new Parser)->parseSourceFile($snippet); 149 | 150 | Settings::$calculatePosition = function (Range $range) use ($node, $safetyPrefix) { 151 | if ($range->start->line === 1) { 152 | $range->start->character -= mb_strlen($safetyPrefix) - 1; 153 | $range->end->character -= mb_strlen($safetyPrefix) - 1; 154 | } 155 | 156 | $range->start->line += $this->startLine + $node->position->startLine - 2; 157 | $range->end->line += $this->startLine + $node->position->startLine - 2; 158 | 159 | return $range; 160 | }; 161 | 162 | $result = Parse::parse($sourceFile); 163 | 164 | $child = $result->children[0]; 165 | 166 | $child->methodName = '@' . substr($child->methodName, mb_strlen($safetyPrefix)); 167 | 168 | $this->items[] = $child; 169 | } 170 | 171 | protected function parseEchoNode(EchoNode $node) 172 | { 173 | $prefix = match ($node->type) { 174 | EchoType::RawEcho => '{!!', 175 | EchoType::TripleEcho => '{{{', 176 | default => '{{', 177 | }; 178 | 179 | $this->doEchoParse($node, $prefix, $node->innerContent); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/Parsers/MemberAccessExpressionParser.php: -------------------------------------------------------------------------------- 1 | context->methodName = $node->memberName->getFullText($node->getRoot()->getFullText()); 23 | 24 | foreach ($node->getDescendantNodes() as $child) { 25 | if ($child instanceof QualifiedName) { 26 | $this->context->className ??= (string) ($child->getResolvedName() ?? $child->getText()); 27 | 28 | return $this->context; 29 | } 30 | 31 | if ($child instanceof Variable) { 32 | if ($child->getName() === 'this') { 33 | $parent = $child->getParent(); 34 | 35 | if ($parent?->getParent() instanceof CallExpression) { 36 | // They are calling a method on the current class 37 | $result = $this->context->nearestClassDefinition(); 38 | 39 | if ($result) { 40 | $this->context->className = $result->className; 41 | } 42 | 43 | continue; 44 | } 45 | 46 | if ($parent instanceof MemberAccessExpression) { 47 | $propName = $parent->memberName->getFullText($node->getRoot()->getFullText()); 48 | 49 | $result = $this->context->searchForProperty($propName); 50 | 51 | if ($result) { 52 | $this->context->className = $result['types'][0] ?? null; 53 | } 54 | } 55 | 56 | continue; 57 | } 58 | 59 | $varName = $child->getName(); 60 | 61 | $result = $this->context->searchForVar($varName); 62 | 63 | if (!$result) { 64 | return $this->context; 65 | } 66 | 67 | if ($result instanceof AssignmentValue) { 68 | $this->context->className = $result->getValue()['name'] ?? null; 69 | } else { 70 | $this->context->className = $result; 71 | } 72 | } 73 | } 74 | 75 | return $this->context; 76 | } 77 | 78 | public function initNewContext(): ?AbstractContext 79 | { 80 | if (!($this->context instanceof MethodCall) || $this->context->methodName !== null) { 81 | return new MethodCall; 82 | } 83 | 84 | return null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/Parsers/MethodDeclarationParser.php: -------------------------------------------------------------------------------- 1 | context->methodName = $node->getName(); 19 | 20 | return $this->context; 21 | } 22 | 23 | public function initNewContext(): ?AbstractContext 24 | { 25 | return new MethodDefinition; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Parsers/ObjectCreationExpressionParser.php: -------------------------------------------------------------------------------- 1 | classTypeDesignator instanceof QualifiedName) { 21 | $this->context->className = (string) $node->classTypeDesignator->getResolvedName(); 22 | } 23 | 24 | $this->context->autocompleting = $node->closeParen instanceof MissingToken; 25 | 26 | if ($node->argumentExpressionList === null) { 27 | return $this->context; 28 | } 29 | 30 | return $this->context->arguments; 31 | } 32 | 33 | public function initNewContext(): ?AbstractContext 34 | { 35 | return new ObjectValue; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Parsers/ParameterDeclarationListParser.php: -------------------------------------------------------------------------------- 1 | context->parameters; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Parsers/ParameterParser.php: -------------------------------------------------------------------------------- 1 | context->name = $node->getName(); 21 | 22 | $constructorProperty = $node->visibilityToken !== null; 23 | 24 | if (!$node->typeDeclarationList) { 25 | if ($constructorProperty) { 26 | $this->context->addPropertyToNearestClassDefinition($this->context->name); 27 | } 28 | 29 | return $this->context->value; 30 | } 31 | 32 | foreach ($node->typeDeclarationList->getValues() as $type) { 33 | if ($type instanceof Token) { 34 | $this->context->types[] = $type->getText($node->getRoot()->getFullText()); 35 | } elseif ($type instanceof QualifiedName) { 36 | $this->context->types[] = (string) $type->getResolvedName(); 37 | } 38 | } 39 | 40 | if ($constructorProperty) { 41 | $this->context->addPropertyToNearestClassDefinition($this->context->name, $this->context->types); 42 | } 43 | 44 | return $this->context->value; 45 | } 46 | 47 | public function initNewContext(): ?AbstractContext 48 | { 49 | return new Parameter; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Parsers/PropertyDeclarationParser.php: -------------------------------------------------------------------------------- 1 | [], 23 | ]; 24 | 25 | $name = null; 26 | 27 | if ($node->propertyElements) { 28 | foreach ($node->propertyElements->getElements() as $element) { 29 | if ($element instanceof Variable) { 30 | $name = $element->getName(); 31 | } 32 | } 33 | } 34 | 35 | if ($node->typeDeclarationList) { 36 | foreach ($node->typeDeclarationList->getValues() as $type) { 37 | if ($type instanceof Token) { 38 | $property['types'][] = $type->getText($node->getRoot()->getFullText()); 39 | } elseif ($type instanceof QualifiedName) { 40 | $property['types'][] = (string) $type->getResolvedName(); 41 | } 42 | } 43 | } 44 | 45 | if ($name !== null) { 46 | $property['name'] = $name; 47 | $this->context->properties[] = $property; 48 | } 49 | 50 | return $this->context; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Parsers/ReturnStatementParser.php: -------------------------------------------------------------------------------- 1 | context->methodName = $node->memberName->getFullText($node->getRoot()->getFullText()); 24 | $this->context->className = $this->resolveClassName($node); 25 | 26 | if ($this->context->methodName === 'class') { 27 | $this->context->methodName = null; 28 | } 29 | 30 | return $this->context; 31 | } 32 | 33 | protected function resolveClassName(ScopedPropertyAccessExpression $node) 34 | { 35 | if (method_exists($node->scopeResolutionQualifier, 'getResolvedName')) { 36 | return (string) $node->scopeResolutionQualifier->getResolvedName(); 37 | } 38 | 39 | if ($node->scopeResolutionQualifier instanceof Variable) { 40 | $result = $this->context->searchForVar($node->scopeResolutionQualifier->getName()); 41 | 42 | if ($result instanceof AssignmentValue) { 43 | return $result->getValue()['name'] ?? null; 44 | } 45 | 46 | return $result; 47 | } 48 | 49 | if ($node->scopeResolutionQualifier instanceof MemberAccessExpression) { 50 | $parser = new MemberAccessExpressionParser; 51 | $context = new MethodCall; 52 | $context->parent = clone $this->context; 53 | $parser->context($context); 54 | $result = $parser->parseNode($node->scopeResolutionQualifier); 55 | 56 | return $result->className ?? null; 57 | } 58 | 59 | return $node->scopeResolutionQualifier->getText(); 60 | } 61 | 62 | public function initNewContext(): ?AbstractContext 63 | { 64 | if ( 65 | $this->context instanceof Argument 66 | || $this->context instanceof AssignmentValue 67 | || $this->context instanceof ArrayItem 68 | ) { 69 | return new MethodCall; 70 | } 71 | 72 | return null; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Parsers/SourceFileNodeParser.php: -------------------------------------------------------------------------------- 1 | loopChildren($node); 12 | 13 | return $this->context; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Parsers/StringLiteralParser.php: -------------------------------------------------------------------------------- 1 | context->value = $node->getStringContentsText(); 21 | 22 | if (Settings::$capturePosition) { 23 | $range = PositionUtilities::getRangeFromPosition( 24 | $node->getStartPosition(), 25 | mb_strlen($node->getStringContentsText()), 26 | $node->getRoot()->getFullText(), 27 | ); 28 | 29 | if (Settings::$calculatePosition !== null) { 30 | $range = Settings::adjustPosition($range); 31 | } 32 | 33 | $this->context->setPosition($range); 34 | } 35 | 36 | return $this->context; 37 | } 38 | 39 | public function initNewContext(): ?AbstractContext 40 | { 41 | return new StringValue; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | $path, 25 | ]); 26 | } 27 | 28 | /** 29 | * Register any application services. 30 | */ 31 | public function register(): void 32 | { 33 | // 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Support/Debugs.php: -------------------------------------------------------------------------------- 1 | debug = true; 13 | } elseif ($this->debug) { 14 | echo PHP_EOL; 15 | echo str_repeat(' ', $this->depth * 2) . '***' . PHP_EOL; 16 | 17 | foreach ($args as $arg) { 18 | $val = var_export($arg, true); 19 | $lines = explode(PHP_EOL, $val); 20 | 21 | foreach ($lines as $line) { 22 | echo str_repeat(' ', $this->depth * 2); 23 | echo $line; 24 | echo PHP_EOL; 25 | } 26 | } 27 | 28 | echo str_repeat(' ', $this->depth * 2) . '***' . PHP_EOL; 29 | echo PHP_EOL; 30 | } 31 | } 32 | 33 | protected function debugNewLine($count = 1, $char = PHP_EOL) 34 | { 35 | if ($this->debug) { 36 | echo str_repeat($char, $count); 37 | } 38 | } 39 | 40 | protected function debugSpacer() 41 | { 42 | if ($this->debug) { 43 | $this->debugNewLine(2); 44 | echo str_repeat('-', 80); 45 | $this->debugNewLine(2); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel/vs-code-php-parser-cli/c4c984dfea0116d325ca246bd3550d925c34a5e6/bin/.gitkeep -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | create(); 6 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | 'VS Code PHP Parser', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Version 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "version" your application is currently running 24 | | in. You may want to follow the "Semantic Versioning" - Given a version 25 | | number MAJOR.MINOR.PATCH when an update happens: https://semver.org. 26 | | 27 | */ 28 | 29 | 'version' => app('git.version'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Environment 34 | |-------------------------------------------------------------------------- 35 | | 36 | | This value determines the "environment" your application is currently 37 | | running in. This may determine how you prefer to configure various 38 | | services the application utilizes. This can be overridden using 39 | | the global command line "--env" option when calling commands. 40 | | 41 | */ 42 | 43 | 'env' => env('ENV', 'development'), 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Autoloaded Service Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | The service providers listed here will be automatically loaded on the 51 | | request to your application. Feel free to add your own services to 52 | | this array to grant expanded functionality to your applications. 53 | | 54 | */ 55 | 56 | 'providers' => [ 57 | App\Providers\AppServiceProvider::class, 58 | ], 59 | 60 | ]; 61 | -------------------------------------------------------------------------------- /config/commands.php: -------------------------------------------------------------------------------- 1 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Commands Paths 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This value determines the "paths" that should be loaded by the console's 28 | | kernel. Foreach "path" present on the array provided below the kernel 29 | | will extract all "Illuminate\Console\Command" based class commands. 30 | | 31 | */ 32 | 33 | 'paths' => [app_path('Commands')], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Added Commands 38 | |-------------------------------------------------------------------------- 39 | | 40 | | You may want to include a single command class without having to load an 41 | | entire folder. Here you can specify which commands should be added to 42 | | your list of commands. The console's kernel will try to load them. 43 | | 44 | */ 45 | 46 | 'add' => [ 47 | // 48 | ], 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Hidden Commands 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Your application commands will always be visible on the application list 56 | | of commands. But you can still make them "hidden" specifying an array 57 | | of commands below. All "hidden" commands can still be run/executed. 58 | | 59 | */ 60 | 61 | 'hidden' => [ 62 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 63 | Symfony\Component\Console\Command\DumpCompletionCommand::class, 64 | Symfony\Component\Console\Command\HelpCommand::class, 65 | Illuminate\Console\Scheduling\ScheduleRunCommand::class, 66 | Illuminate\Console\Scheduling\ScheduleListCommand::class, 67 | Illuminate\Console\Scheduling\ScheduleFinishCommand::class, 68 | Illuminate\Foundation\Console\VendorPublishCommand::class, 69 | LaravelZero\Framework\Commands\StubPublishCommand::class, 70 | CompileBinaryCommand::class, 71 | ContextTypeScriptGeneratorCommand::class, 72 | Tag::class, 73 | ], 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Removed Commands 78 | |-------------------------------------------------------------------------- 79 | | 80 | | Do you have a service provider that loads a list of commands that 81 | | you don't need? No problem. Laravel Zero allows you to specify 82 | | below a list of commands that you don't to see in your app. 83 | | 84 | */ 85 | 86 | 'remove' => [ 87 | // 88 | ], 89 | 90 | ]; 91 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'null'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Deprecations Log Channel 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This option controls the log channel that should be used to log warnings 29 | | regarding deprecated PHP and library features. This allows you to get 30 | | your application ready for upcoming major versions of dependencies. 31 | | 32 | */ 33 | 34 | 'deprecations' => [ 35 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 36 | 'trace' => env('LOG_DEPRECATIONS_TRACE', false), 37 | ], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Log Channels 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Here you may configure the log channels for your application. Laravel 45 | | utilizes the Monolog PHP logging library, which includes a variety 46 | | of powerful log handlers and formatters that you're free to use. 47 | | 48 | | Available Drivers: "single", "daily", "slack", "syslog", 49 | | "errorlog", "monolog", "custom", "stack" 50 | | 51 | */ 52 | 53 | 'channels' => [ 54 | 55 | 'stack' => [ 56 | 'driver' => 'stack', 57 | 'channels' => explode(',', env('LOG_STACK', 'single')), 58 | 'ignore_exceptions' => false, 59 | ], 60 | 61 | 'single' => [ 62 | 'driver' => 'single', 63 | 'path' => storage_path('logs/laravel.log'), 64 | 'level' => env('LOG_LEVEL', 'debug'), 65 | 'replace_placeholders' => true, 66 | ], 67 | 68 | 'daily' => [ 69 | 'driver' => 'daily', 70 | 'path' => storage_path('logs/laravel.log'), 71 | 'level' => env('LOG_LEVEL', 'debug'), 72 | 'days' => env('LOG_DAILY_DAYS', 14), 73 | 'replace_placeholders' => true, 74 | ], 75 | 76 | 'slack' => [ 77 | 'driver' => 'slack', 78 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 79 | 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), 80 | 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 81 | 'level' => env('LOG_LEVEL', 'critical'), 82 | 'replace_placeholders' => true, 83 | ], 84 | 85 | 'papertrail' => [ 86 | 'driver' => 'monolog', 87 | 'level' => env('LOG_LEVEL', 'debug'), 88 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 89 | 'handler_with' => [ 90 | 'host' => env('PAPERTRAIL_URL'), 91 | 'port' => env('PAPERTRAIL_PORT'), 92 | 'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'), 93 | ], 94 | 'processors' => [PsrLogMessageProcessor::class], 95 | ], 96 | 97 | 'stderr' => [ 98 | 'driver' => 'monolog', 99 | 'level' => env('LOG_LEVEL', 'debug'), 100 | 'handler' => StreamHandler::class, 101 | 'formatter' => env('LOG_STDERR_FORMATTER'), 102 | 'with' => [ 103 | 'stream' => 'php://stderr', 104 | ], 105 | 'processors' => [PsrLogMessageProcessor::class], 106 | ], 107 | 108 | 'syslog' => [ 109 | 'driver' => 'syslog', 110 | 'level' => env('LOG_LEVEL', 'debug'), 111 | 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 112 | 'replace_placeholders' => true, 113 | ], 114 | 115 | 'errorlog' => [ 116 | 'driver' => 'errorlog', 117 | 'level' => env('LOG_LEVEL', 'debug'), 118 | 'replace_placeholders' => true, 119 | ], 120 | 121 | 'null' => [ 122 | 'driver' => 'monolog', 123 | 'handler' => NullHandler::class, 124 | ], 125 | 126 | 'emergency' => [ 127 | 'path' => storage_path('logs/laravel.log'), 128 | ], 129 | 130 | ], 131 | 132 | ]; 133 | -------------------------------------------------------------------------------- /fix-class-resolutions.php: -------------------------------------------------------------------------------- 1 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | ./tests/Unit 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "exclude": [ 4 | "bin", 5 | "source", 6 | "downloads", 7 | "builds", 8 | "buildroot", 9 | "tests/snippets" 10 | ], 11 | "rules": { 12 | "concat_space": { 13 | "spacing": "one" 14 | }, 15 | "not_operator_with_successor_space": false, 16 | "binary_operator_spaces": { 17 | "operators": { 18 | "=": "single_space", 19 | "=>": "align" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /snippets/anon-func.php: -------------------------------------------------------------------------------- 1 | whereIn(' 18 | -------------------------------------------------------------------------------- /snippets/anonymous-function-param.php: -------------------------------------------------------------------------------- 1 | whereIn(' 8 | -------------------------------------------------------------------------------- /snippets/array-with-arrow-function-missing-second-key.php: -------------------------------------------------------------------------------- 1 | fn(Builder $q) => $q->where(''), 8 | ' 9 | -------------------------------------------------------------------------------- /snippets/array-with-arrow-function-several-keys-and-second-param.php: -------------------------------------------------------------------------------- 1 | fn(Builder $q) => $q->where('',''), 8 | 'organization' => fn($q) => $q->whereIn('', ' 9 | -------------------------------------------------------------------------------- /snippets/array-with-arrow-function-several-keys.php: -------------------------------------------------------------------------------- 1 | fn(Builder $q) => $q->where('',''), 8 | 'organization' => fn($q) => $q->whereIn(' 9 | -------------------------------------------------------------------------------- /snippets/array-with-arrow-function.php: -------------------------------------------------------------------------------- 1 | fn(Builder $q) => $q->where(' 8 | -------------------------------------------------------------------------------- /snippets/arrow-function-param.php: -------------------------------------------------------------------------------- 1 | $q->whereIn(' 7 | -------------------------------------------------------------------------------- /snippets/basic-function-with-param.php: -------------------------------------------------------------------------------- 1 | where('email', ' 8 | -------------------------------------------------------------------------------- /snippets/basic-method.php: -------------------------------------------------------------------------------- 1 | where(' 8 | -------------------------------------------------------------------------------- /snippets/basic-static-method-with-params.php: -------------------------------------------------------------------------------- 1 | where('url', $url) 15 | ->whereNotIn('ip_address1', $ipAddresses1->toArray()) 16 | ->whereNotIn('ip_address2', $ipAddresses2->toArray()) 17 | ->whereNotIn('ip_address3', $ipAddresses3->toArray()) 18 | ->with('records', ['another'], ['my_key' => 'my_value'], ' 19 | -------------------------------------------------------------------------------- /snippets/chained-method-with-params.php: -------------------------------------------------------------------------------- 1 | where('email', '')->orWhere('name', ' 8 | -------------------------------------------------------------------------------- /snippets/chained-static-method-with-params.php: -------------------------------------------------------------------------------- 1 | orWhere('name', ' 6 | -------------------------------------------------------------------------------- /snippets/chained-static-method.php: -------------------------------------------------------------------------------- 1 | whereNotIn('ip_address1', $ipAddresses1->toArray()) 18 | ->whereNotIn('ip_address2', $ipAddresses2->toArray()) 19 | ->whereNotIn('ip_address3', $ipAddresses3->toArray()) 20 | ->with('records', ['another'], ['my_key' => 'my_value'], ' 21 | -------------------------------------------------------------------------------- /snippets/class-def.php: -------------------------------------------------------------------------------- 1 | Providers::all(), 9 | 'isFirstProvider' => auth()->user()->providers()->count() === 0, 10 | 'jsonPolicy' => file_get_contents(resource_path('providers/aws/policy.json')), 11 | ]); 12 | -------------------------------------------------------------------------------- /snippets/function-def.php: -------------------------------------------------------------------------------- 1 | user->where('url', $url) 17 | ->whereNotIn('ip_address1', $ipAddresses1->toArray()) 18 | ->whereNotIn('ip_address2', $ipAddresses2->toArray()) 19 | ->whereNotIn('ip_address3', $ipAddresses3->toArray()) 20 | ->with('records', ['another'], ['my_key' => 'my_value'], ' 21 | -------------------------------------------------------------------------------- /snippets/route-view.php: -------------------------------------------------------------------------------- 1 | '', ' 4 | 5 | // User::with([ 6 | // 'records' => function($q) { $q->where('')->orWhere(' 7 | 8 | // User::where('email', 'whatever')->where(function ($q) { 9 | // $q->where(''); 10 | // })->with([ 11 | // 'currentTeam' => function ($q) { 12 | // $q->where(''); 13 | // }, 14 | // ' 15 | 16 | 17 | User::where('email', 'whatever')->with([ 18 | 'currentTeam' => fn($q) => $q->where(''), 19 | ' 20 | -------------------------------------------------------------------------------- /snippets/var-chain-in-method-def.php: -------------------------------------------------------------------------------- 1 | where('url', $url) 17 | ->whereNotIn('ip_address1', $ipAddresses1->toArray()) 18 | ->whereNotIn('ip_address2', $ipAddresses2->toArray()) 19 | ->whereNotIn('ip_address3', $ipAddresses3->toArray()) 20 | ->with('records', ['another'], ['my_key' => 'my_value'], ' 21 | -------------------------------------------------------------------------------- /snippets/wild.php: -------------------------------------------------------------------------------- 1 | client = app(MultiRegionClient::class, [ 45 | 'args' => [ 46 | 'credentials' => new Credentials($provider->credentials['key'], $provider->credentials['secret']), 47 | 'version' => 'latest', 48 | 'service' => 'ec2', 49 | ], 50 | ]); 51 | 52 | $this->addedRules = collect(); 53 | $this->dbProvider = $provider; 54 | } 55 | 56 | public function getSecurityGroupById(string $id, string $region = null): ProviderFirewall 57 | { 58 | $awsSecurityGroup = $this->client->describeSecurityGroups([ 59 | 'Filters' => [ 60 | [ 61 | 'Name' => 'group-id', 62 | 'Values' => [$id], 63 | ], 64 | ], 65 | '@region' => $region, 66 | ]); 67 | 68 | if (!count($awsSecurityGroup->get('SecurityGroups'))) { 69 | return new ProviderFirewall( 70 | id: $id, 71 | name: '[Security Group not found]', 72 | ); 73 | } 74 | 75 | return new ProviderFirewall( 76 | id: $awsSecurityGroup->get('SecurityGroups')[0]['GroupId'], 77 | name: $awsSecurityGroup->get('SecurityGroups')[0]['GroupName'], 78 | ); 79 | } 80 | 81 | public function getProviderFirewallById(string $id, string $region = null): ProviderFirewall|false 82 | { 83 | return $this->getSecurityGroupById($id, $region); 84 | } 85 | 86 | public function test(): bool 87 | { 88 | try { 89 | $this->client->describeSecurityGroups([ 90 | 'DryRun' => true, 91 | '@region' => 'us-east-1', 92 | ]); 93 | } catch (Ec2Exception $e) { 94 | // If we have the permissions, but it's a dry run, we're cool 95 | return $e->getAwsErrorCode() === 'DryRunOperation'; 96 | } catch (\Exception $e) { 97 | return false; 98 | } 99 | 100 | return true; 101 | } 102 | 103 | public function securityGroups(string $region): Collection 104 | { 105 | return collect($this->client->describeSecurityGroups([ 106 | '@region' => $region, 107 | ])->get('SecurityGroups'))->map( 108 | fn ($group) => new ProviderFirewall( 109 | id: $group['GroupId'], 110 | name: $group['GroupName'], 111 | description: $group['Description'] ?: null, 112 | ), 113 | )->sortBy(fn ($f) => strtolower($f->name))->values(); 114 | } 115 | 116 | /** 117 | * @return \Illuminate\Support\Collection 118 | */ 119 | public function regions(): Collection 120 | { 121 | $sdkRegionsPath = base_path('vendor/aws/aws-sdk-php/src/data/endpoints.json.php'); 122 | 123 | if (!file_exists($sdkRegionsPath)) { 124 | throw new \Exception('SDK Regions path does not exist!'); 125 | 126 | return collect(); 127 | } 128 | 129 | $result = $this->client->describeRegions([ 130 | '@region' => $this->dbProvider->default_region ?: 'us-east-1', 131 | ]); 132 | 133 | $sdkConfig = require $sdkRegionsPath; 134 | 135 | $sdkRegions = collect($sdkConfig['partitions'])->flatMap(fn ($p) => $p['regions']); 136 | 137 | return collect($result->get('Regions'))->map(fn ($r) => new ProviderRegion( 138 | id: $r['RegionName'], 139 | name: $sdkRegions->offsetGet($r['RegionName'])['description'], 140 | ))->sortBy('id')->values(); 141 | } 142 | 143 | public function validateAddRule(RuleValidation $rule): void 144 | { 145 | $allowManagementIds = collect(request()->input('let_blip_manage_rule_ids')); 146 | 147 | // Let's do some integrity checking up front and inform user if we encounter anything instead of adding and rolling back 148 | $this->validator = new AWSValidator( 149 | client: $this->client, 150 | rule: $rule, 151 | allowManagementIds: $allowManagementIds, 152 | ); 153 | 154 | $this->validator->validate(); 155 | 156 | if ($this->validator->problems->count()) { 157 | throw ValidationException::withMessages([ 158 | 'existingRules' => $this->validator->problems, 159 | ]); 160 | } 161 | } 162 | 163 | public function getProviderResourceById(string $id, string $region = null): ProviderResource 164 | { 165 | $securityGroup = $this->getSecurityGroupById($id, $region); 166 | 167 | return new ProviderResource( 168 | id: $securityGroup->id, 169 | name: $securityGroup->name, 170 | ); 171 | } 172 | 173 | public function getDefaultRegion(): ?string 174 | { 175 | // We're only looking for one, but must be an object so that 176 | // closure will reflect changes [sigh] 177 | $defaultRegion = collect(); 178 | 179 | // Sort the US ones first to see if we can find a result faster, based on likely demographic 180 | $sortedRegions = $this->regions()->sortBy(fn ($r) => Str::startsWith($r->id, 'us-') ? -1 : 0); 181 | 182 | $pools = $sortedRegions 183 | ->map(fn (ProviderRegion $r) => $this->client->getCommand('DescribeSecurityGroups', [ 184 | '@region' => $r->id, 185 | 'MaxResults' => 5, 186 | ])) 187 | ->chunk(10) 188 | ->map(fn ($commands) => new CommandPool($this->client, $commands, [ 189 | 'fulfilled' => function ( 190 | ResultInterface $result, 191 | $iterKey, 192 | PromiseInterface $aggregatePromise, 193 | ) use ($defaultRegion, $commands) { 194 | if ($defaultRegion->count() !== 0) { 195 | $aggregatePromise->cancel(); 196 | // We already found one, we're good 197 | return; 198 | } 199 | 200 | // Let's try and find something with more than just the default security group, that's a sensible default 201 | if (count($result->get('SecurityGroups')) > 1) { 202 | $defaultRegion->push( 203 | $commands->values()[$iterKey]->offsetGet('@region'), 204 | ); 205 | 206 | $aggregatePromise->cancel(); 207 | } 208 | }, 209 | ])); 210 | 211 | foreach ($pools as $pool) { 212 | try { 213 | $pool->promise()->wait(); 214 | } catch (CancellationException $e) { 215 | // We're good, we cancelled the remaining 216 | // requests since we found one already 217 | } 218 | 219 | if ($defaultRegion->count() > 0) { 220 | return $defaultRegion->first(); 221 | } 222 | } 223 | 224 | // Default to the first region, they can change it later 225 | return $sortedRegions->first()->id; 226 | } 227 | 228 | public function rollback(): void 229 | { 230 | $this->addedRules->each(function ($rule) { 231 | $this->client->revokeSecurityGroupIngress([ 232 | 'GroupId' => $rule['security_group_id'], 233 | 'SecurityGroupRuleIds' => [$rule['security_group_rule_id']], 234 | '@region' => $rule['region'], 235 | ]); 236 | }); 237 | } 238 | 239 | public function syncIpsToFirewall( 240 | string $firewallId, 241 | Collection $ips, 242 | string $protocol, 243 | string $port, 244 | string $ruleDescription, 245 | string $region = null, 246 | ?Collection $oldIps = null, 247 | ): IpSyncDiff { 248 | $result = $this->client->describeSecurityGroupRules([ 249 | 'Filters' => [ 250 | [ 251 | 252 | 'Name' => 'group-id', 253 | 'Values' => [$firewallId], 254 | ], 255 | ], 256 | '@region' => $region, 257 | ]); 258 | 259 | $existingRules = collect($result->get('SecurityGroupRules')) 260 | ->filter(fn ($r) => !$r['IsEgress']) 261 | ->filter(fn ($r) => $r['IpProtocol'] === AWSHelper::getProtocol($protocol)) 262 | ->filter(fn ($r) => (string) $r['FromPort'] === AWSHelper::getFromPort($port) 263 | && (string) $r['ToPort'] === AWSHelper::getToPort($port)) 264 | ->values(); 265 | 266 | $existingIpAddresses = $existingRules->map(fn ($r) => AWSHelper::getIpFromRule($r)); 267 | 268 | $awsIps = $ips->map(fn ($ip) => AWSHelper::getIp($ip)); 269 | $oldAwsIps = $oldIps->map(fn ($ip) => AWSHelper::getIp($ip)); 270 | 271 | $toRemove = $existingRules->filter( 272 | fn ($r) => $oldAwsIps->contains(AWSHelper::getIpFromRule($r)) 273 | )->values(); 274 | $toAdd = $awsIps->diff($existingIpAddresses)->values(); 275 | 276 | if ($toRemove->count() > 0) { 277 | $ruleIds = $toRemove->pluck('SecurityGroupRuleId'); 278 | 279 | $this->client->revokeSecurityGroupIngress([ 280 | 'GroupId' => $firewallId, 281 | 'SecurityGroupRuleIds' => $ruleIds->toArray(), 282 | '@region' => $region, 283 | ]); 284 | } 285 | 286 | if ($toAdd->count() > 0) { 287 | $params = [ 288 | '@region' => $region, 289 | 'GroupId' => $firewallId, 290 | 'IpPermissions' => [ 291 | [ 292 | 'IpProtocol' => AWSHelper::getProtocol($protocol), 293 | 'FromPort' => AWSHelper::getFromPort($port), 294 | 'ToPort' => AWSHelper::getToPort($port), 295 | 'IpRanges' => $toAdd->filter(fn ($r) => Ip::isIpv4($r))->values()->map(fn ($ip) => [ 296 | 'CidrIp' => $ip, 297 | 'Description' => $ruleDescription, 298 | ])->toArray(), 299 | 'Ipv6Ranges' => $toAdd->filter(fn ($r) => Ip::isIpv6($r))->values()->map(fn ($ip) => [ 300 | 'CidrIpv6' => $ip, 301 | 'Description' => $ruleDescription, 302 | ])->toArray(), 303 | ], 304 | ], 305 | ]; 306 | 307 | if (count($params['IpPermissions'][0]['IpRanges']) === 0) { 308 | unset($params['IpPermissions'][0]['IpRanges']); 309 | } 310 | 311 | if (count($params['IpPermissions'][0]['Ipv6Ranges']) === 0) { 312 | unset($params['IpPermissions'][0]['Ipv6Ranges']); 313 | } 314 | 315 | try { 316 | $this->client->authorizeSecurityGroupIngress($params); 317 | } catch (\Exception $e) { 318 | Bugsnag::notifyException($e); 319 | } 320 | } 321 | 322 | return new IpSyncDiff( 323 | added: $toAdd, 324 | removed: $toRemove->map(fn ($r) => AWSHelper::getIpFromRule($r)), 325 | ); 326 | } 327 | 328 | protected function addFirewallRule(Rule $rule): Rule 329 | { 330 | $rule->securityGroups->each(function (RuleSecurityGroup $securityGroup) use ($rule) { 331 | $ruleName = $rule->name ? 332 | Str::of($rule->name)->pipe(fn ($s) => preg_replace('/[^\da-z ]/i', '', $s))->limit(100)->wrap('(', ')')->toString() 333 | : ''; 334 | 335 | $params = [ 336 | 'GroupId' => $securityGroup->security_group_id, 337 | 'IpPermissions' => [ 338 | [ 339 | 'IpProtocol' => AWSHelper::getProtocol($rule->protocol), 340 | 'FromPort' => AWSHelper::getFromPort($rule->port), 341 | 'ToPort' => AWSHelper::getToPort($rule->port), 342 | 'IpRanges' => [ 343 | [ 344 | 'CidrIp' => AWSHelper::getIp($rule->ip_address), 345 | 'Description' => trim( 346 | sprintf( 347 | 'Created by %s %s', 348 | config('app.name'), 349 | $ruleName, 350 | ), 351 | ), 352 | ], 353 | ], 354 | ], 355 | ], 356 | '@region' => $rule->region, 357 | ]; 358 | 359 | try { 360 | $result = $this->client->authorizeSecurityGroupIngress($params); 361 | 362 | $this->addedRules->push([ 363 | 'security_group_id' => $securityGroup->security_group_id, 364 | 'security_group_rule_id' => $result->get('SecurityGroupRules')[0]['SecurityGroupRuleId'], 365 | 'region' => $rule->region, 366 | ]); 367 | 368 | // $securityGroup->rules()->save(new RuleSecurityGroupRule([' 369 | // $securityGroup->rules()->save(' 370 | $securityGroup->save(' 371 | -------------------------------------------------------------------------------- /snippets/with-array-funcs.php: -------------------------------------------------------------------------------- 1 | with([ 21 | 'team'=> function($q) { 22 | $q->where('name', 'Manager'); 23 | }, 24 | 'org' => function ($q) { 25 | $q->where('company' 26 | // 'team'=> fn($q) =>$q->where('name', 'Manager'), 27 | // 'org' => fn ($q) => $q->where('company' 28 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Unit'); 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Expectations 19 | |-------------------------------------------------------------------------- 20 | | 21 | | When you're writing tests, you often need to check that values meet certain conditions. The 22 | | "expect()" function gives you access to a set of "expectations" methods that you can use 23 | | to assert different things. Of course, you may extend the Expectation API at any time. 24 | | 25 | */ 26 | 27 | // expect()->extend('toBeOne', function () { 28 | // return $this->toBe(1); 29 | // }); 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Functions 34 | |-------------------------------------------------------------------------- 35 | | 36 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 37 | | project that you don't want to repeat in every file. Here you can also expose helpers as 38 | | global functions to help you to reduce the number of lines of code in your test files. 39 | | 40 | */ 41 | 42 | // function something(): void 43 | // { 44 | // // .. 45 | // } 46 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | array_merge([ 13 | 'class' => null, 14 | 'method' => null, 15 | 'params' => [], 16 | ], $v), $values); 17 | } 18 | 19 | function result($file) 20 | { 21 | $code = fromFile($file); 22 | $walker = new DetectWalker($code); 23 | 24 | $context = $walker->walk(); 25 | 26 | return $context->toJson(JSON_PRETTY_PRINT); 27 | } 28 | 29 | test('extract functions and string params', function () { 30 | expect(result('detect/routes'))->toBe(detect([ 31 | 32 | [ 33 | [ 34 | 'method' => 'basicFunc', 35 | 'class' => null, 36 | 'params' => [ 37 | [ 38 | 'type' => 'string', 39 | 'value' => 'whatever', 40 | 'start' => [ 41 | 'line' => 9, 42 | 'column' => 10, 43 | ], 44 | 'end' => [ 45 | 'line' => 9, 46 | 'column' => 18, 47 | ], 48 | ], 49 | ], 50 | ], 51 | [ 52 | 'method' => 'name', 53 | 'class' => 'Illuminate\\Support\\Facades\\Route', 54 | 'params' => [ 55 | [ 56 | 'type' => 'string', 57 | 'value' => 'home.show', 58 | 'start' => [ 59 | 'line' => 11, 60 | 'column' => 55, 61 | ], 62 | 'end' => [ 63 | 'line' => 11, 64 | 'column' => 64, 65 | ], 66 | ], 67 | ], 68 | ], 69 | [ 70 | 'method' => 'get', 71 | 'class' => 'Illuminate\\Support\\Facades\\Route', 72 | 'params' => [ 73 | [ 74 | 'type' => 'string', 75 | 'value' => '/', 76 | 'start' => [ 77 | 'line' => 11, 78 | 'column' => 11, 79 | ], 80 | 'end' => [ 81 | 'line' => 11, 82 | 'column' => 12, 83 | ], 84 | ], 85 | [ 86 | 'type' => 'array', 87 | 'value' => [ 88 | [ 89 | 'key' => [ 90 | 'type' => 'null', 91 | 'value' => null, 92 | ], 93 | 'value' => [ 94 | 'type' => 'unknown', 95 | 'value' => 'HomeController::class', 96 | ], 97 | ], 98 | [ 99 | 'key' => [ 100 | 'type' => 'null', 101 | 'value' => null, 102 | ], 103 | 'value' => [ 104 | 'type' => 'string', 105 | 'value' => 'show', 106 | 'start' => [ 107 | 'line' => 11, 108 | 'column' => 40, 109 | ], 110 | 'end' => [ 111 | 'line' => 11, 112 | 'column' => 44, 113 | ], 114 | ], 115 | ], 116 | ], 117 | ], 118 | ], 119 | ], 120 | [ 121 | 'method' => 'group', 122 | 'class' => 'Illuminate\\Support\\Facades\\Route', 123 | 'params' => [ 124 | [ 125 | 'type' => 'closure', 126 | 'arguments' => [], 127 | ], 128 | ], 129 | ], 130 | [ 131 | 'method' => 'middleware', 132 | 'class' => 'Illuminate\\Support\\Facades\\Route', 133 | 'params' => [ 134 | [ 135 | 'type' => 'string', 136 | 'value' => 'signed', 137 | 'start' => [ 138 | 'line' => 13, 139 | 'column' => 18, 140 | ], 141 | 'end' => [ 142 | 'line' => 13, 143 | 'column' => 24, 144 | ], 145 | ], 146 | ], 147 | ], 148 | [ 149 | 'method' => 'name', 150 | 'class' => 'Illuminate\\Support\\Facades\\Route', 151 | 'params' => [ 152 | [ 153 | 'type' => 'string', 154 | 'value' => 'profile.edit', 155 | 'start' => [ 156 | 'line' => 15, 157 | 'column' => 68, 158 | ], 159 | 'end' => [ 160 | 'line' => 15, 161 | 'column' => 80, 162 | ], 163 | ], 164 | ], 165 | ], 166 | [ 167 | 'method' => 'get', 168 | 'class' => 'Illuminate\\Support\\Facades\\Route', 169 | 'params' => [ 170 | [ 171 | 'type' => 'string', 172 | 'value' => 'profile', 173 | 'start' => [ 174 | 'line' => 15, 175 | 'column' => 15, 176 | ], 177 | 'end' => [ 178 | 'line' => 15, 179 | 'column' => 22, 180 | ], 181 | ], 182 | [ 183 | 'type' => 'array', 184 | 'value' => [ 185 | [ 186 | 'key' => [ 187 | 'type' => 'null', 188 | 'value' => null, 189 | ], 190 | 'value' => [ 191 | 'type' => 'unknown', 192 | 'value' => 'ProfileController::class', 193 | ], 194 | ], 195 | [ 196 | 'key' => [ 197 | 'type' => 'null', 198 | 'value' => null, 199 | ], 200 | 'value' => [ 201 | 'type' => 'string', 202 | 'value' => 'edit', 203 | 'start' => [ 204 | 'line' => 15, 205 | 'column' => 53, 206 | ], 207 | 'end' => [ 208 | 'line' => 15, 209 | 'column' => 57, 210 | ], 211 | ], 212 | ], 213 | ], 214 | ], 215 | ], 216 | ], 217 | [ 218 | 'method' => 'name', 219 | 'class' => 'Illuminate\\Support\\Facades\\Route', 220 | 'params' => [ 221 | [ 222 | 'type' => 'string', 223 | 'value' => 'profile.edit', 224 | 'start' => [ 225 | 'line' => 15, 226 | 'column' => 68, 227 | ], 228 | 'end' => [ 229 | 'line' => 15, 230 | 'column' => 80, 231 | ], 232 | ], 233 | ], 234 | ], 235 | [ 236 | 'method' => 'get', 237 | 'class' => 'Illuminate\\Support\\Facades\\Route', 238 | 'params' => [ 239 | [ 240 | 'type' => 'string', 241 | 'value' => 'profile', 242 | 'start' => [ 243 | 'line' => 15, 244 | 'column' => 15, 245 | ], 246 | 'end' => [ 247 | 'line' => 15, 248 | 'column' => 22, 249 | ], 250 | ], 251 | [ 252 | 'type' => 'array', 253 | 'value' => [ 254 | [ 255 | 'key' => [ 256 | 'type' => 'null', 257 | 'value' => null, 258 | ], 259 | 'value' => [ 260 | 'type' => 'unknown', 261 | 'value' => 'ProfileController::class', 262 | ], 263 | ], 264 | [ 265 | 'key' => [ 266 | 'type' => 'null', 267 | 'value' => null, 268 | ], 269 | 'value' => [ 270 | 'type' => 'string', 271 | 'value' => 'edit', 272 | 'start' => [ 273 | 'line' => 15, 274 | 'column' => 53, 275 | ], 276 | 'end' => [ 277 | 'line' => 15, 278 | 'column' => 57, 279 | ], 280 | ], 281 | ], 282 | ], 283 | ], 284 | ], 285 | ], 286 | [ 287 | 'method' => 'group', 288 | 'class' => 'Illuminate\\Support\\Facades\\Route', 289 | 'params' => [ 290 | [ 291 | 'type' => 'closure', 292 | 'arguments' => [], 293 | ], 294 | ], 295 | ], 296 | [ 297 | 'method' => 'middleware', 298 | 'class' => 'Illuminate\\Support\\Facades\\Route', 299 | 'params' => [ 300 | [ 301 | 'type' => 'array', 302 | 'value' => [ 303 | [ 304 | 'key' => [ 305 | 'type' => 'null', 306 | 'value' => null, 307 | ], 308 | 'value' => [ 309 | 'type' => 'string', 310 | 'value' => 'auth', 311 | 'start' => [ 312 | 'line' => 19, 313 | 'column' => 4, 314 | ], 315 | 'end' => [ 316 | 'line' => 19, 317 | 'column' => 8, 318 | ], 319 | ], 320 | ], 321 | [ 322 | 'key' => [ 323 | 'type' => 'null', 324 | 'value' => null, 325 | ], 326 | 'value' => [ 327 | 'type' => 'string', 328 | 'value' => 'verified', 329 | 'start' => [ 330 | 'line' => 20, 331 | 'column' => 4, 332 | ], 333 | 'end' => [ 334 | 'line' => 20, 335 | 'column' => 12, 336 | ], 337 | ], 338 | ], 339 | [ 340 | 'key' => [ 341 | 'type' => 'null', 342 | 'value' => null, 343 | ], 344 | 'value' => [ 345 | 'type' => 'string', 346 | 'value' => 'within-current-organization', 347 | 'start' => [ 348 | 'line' => 21, 349 | 'column' => 4, 350 | ], 351 | 'end' => [ 352 | 'line' => 21, 353 | 'column' => 31, 354 | ], 355 | ], 356 | ], 357 | ], 358 | ], 359 | ], 360 | ], 361 | [ 362 | 'method' => 'name', 363 | 'class' => 'Illuminate\\Support\\Facades\\Route', 364 | 'params' => [ 365 | [ 366 | 'type' => 'string', 367 | 'value' => 'dashboard', 368 | 'start' => [ 369 | 'line' => 23, 370 | 'column' => 72, 371 | ], 372 | 'end' => [ 373 | 'line' => 23, 374 | 'column' => 81, 375 | ], 376 | ], 377 | ], 378 | ], 379 | [ 380 | 'method' => 'get', 381 | 'class' => 'Illuminate\\Support\\Facades\\Route', 382 | 'params' => [ 383 | [ 384 | 'type' => 'string', 385 | 'value' => 'dashboard', 386 | 'start' => [ 387 | 'line' => 23, 388 | 'column' => 15, 389 | ], 390 | 'end' => [ 391 | 'line' => 23, 392 | 'column' => 81, 393 | ], 394 | ], 395 | ], 396 | ], 397 | [ 398 | 'method' => 'name', 399 | 'class' => 'Illuminate\\Support\\Facades\\Route', 400 | 'params' => [ 401 | [ 402 | 'type' => 'string', 403 | 'value' => 'dashboard', 404 | 'start' => [ 405 | 'line' => 23, 406 | 'column' => 72, 407 | ], 408 | 'end' => [ 409 | 'line' => 23, 410 | 'column' => 81, 411 | ], 412 | ], 413 | ], 414 | ], 415 | [ 416 | 'method' => 'get', 417 | 'class' => 'Illuminate\\Support\\Facades\\Route', 418 | 'params' => [ 419 | [ 420 | 'type' => 'string', 421 | 'value' => 'dashboard', 422 | 'start' => [ 423 | 'line' => 23, 424 | 'column' => 15, 425 | ], 426 | 'end' => [ 427 | 'line' => 23, 428 | 'column' => 81, 429 | ], 430 | ], 431 | [ 432 | 'type' => 'array', 433 | 'value' => [ 434 | [ 435 | 'key' => [ 436 | 'type' => 'null', 437 | 'value' => null, 438 | ], 439 | 'value' => [ 440 | 'type' => 'unknown', 441 | 'value' => 'DashboardController::class', 442 | ], 443 | ], 444 | [ 445 | 'key' => [ 446 | 'type' => 'null', 447 | 'value' => null, 448 | ], 449 | 'value' => [ 450 | 'type' => 'string', 451 | 'value' => 'show', 452 | 'start' => [ 453 | 'line' => 23, 454 | 'column' => 57, 455 | ], 456 | 'end' => [ 457 | 'line' => 23, 458 | 'column' => 61, 459 | ], 460 | ], 461 | ], 462 | ], 463 | ], 464 | ], 465 | ], 466 | [ 467 | 'method' => 'name', 468 | 'class' => 'Illuminate\\Support\\Facades\\Route', 469 | 'params' => [ 470 | [ 471 | 'type' => 'string', 472 | 'value' => 'gitlab.webhook.store', 473 | 'start' => [ 474 | 'line' => 30, 475 | 'column' => 11, 476 | ], 477 | 'end' => [ 478 | 'line' => 30, 479 | 'column' => 31, 480 | ], 481 | ], 482 | ], 483 | ], 484 | [ 485 | 'method' => 'middleware', 486 | 'class' => 'Illuminate\\Support\\Facades\\Route', 487 | 'params' => [ 488 | [ 489 | 'type' => 'unknown', 490 | 'value' => 'VerifyGitLabWebhookRequest::class', 491 | ], 492 | ], 493 | ], 494 | [ 495 | 'method' => 'withoutMiddleware', 496 | 'class' => 'Illuminate\\Support\\Facades\\Route', 497 | 'params' => [ 498 | [ 499 | 'type' => 'string', 500 | 'value' => 'web', 501 | 'start' => [ 502 | 'line' => 28, 503 | 'column' => 24, 504 | ], 505 | 'end' => [ 506 | 'line' => 28, 507 | 'column' => 27, 508 | ], 509 | ], 510 | ], 511 | ], 512 | [ 513 | 'method' => 'post', 514 | 'class' => 'Illuminate\\Support\\Facades\\Route', 515 | 'params' => [ 516 | [ 517 | 'type' => 'string', 518 | 'value' => 'gitlab/webhook', 519 | 'start' => [ 520 | 'line' => 27, 521 | 'column' => 12, 522 | ], 523 | 'end' => [ 524 | 'line' => 27, 525 | 'column' => 26, 526 | ], 527 | ], 528 | [ 529 | 'type' => 'array', 530 | 'value' => [ 531 | [ 532 | 'key' => [ 533 | 'type' => 'null', 534 | 'value' => null, 535 | ], 536 | 'value' => [ 537 | 'type' => 'unknown', 538 | 'value' => 'GitLabWebhookController::class', 539 | ], 540 | ], 541 | [ 542 | 'key' => [ 543 | 'type' => 'null', 544 | 'value' => null, 545 | ], 546 | 'value' => [ 547 | 'type' => 'string', 548 | 'value' => 'store', 549 | 'start' => [ 550 | 'line' => 27, 551 | 'column' => 63, 552 | ], 553 | 'end' => [ 554 | 'line' => 27, 555 | 'column' => 68, 556 | ], 557 | ], 558 | ], 559 | ], 560 | ], 561 | ], 562 | ], 563 | ], 564 | 565 | ])); 566 | }); 567 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/Unit/ParserTest.php: -------------------------------------------------------------------------------- 1 | 'base', 'children' => $values], JSON_PRETTY_PRINT); 13 | } 14 | 15 | function contextFromArray($values) 16 | { 17 | return array_merge([ 18 | 'classDefinition' => null, 19 | 'implements' => [], 20 | 'extends' => null, 21 | 'methodDefinition' => null, 22 | 'methodDefinitionParams' => [], 23 | 'methodExistingArgs' => [], 24 | 'classUsed' => null, 25 | 'methodUsed' => null, 26 | 'parent' => null, 27 | 'variables' => [], 28 | 'definedProperties' => [], 29 | 'fillingInArrayKey' => false, 30 | 'fillingInArrayValue' => false, 31 | 'paramIndex' => 0, 32 | ], $values); 33 | } 34 | 35 | function contextResult($file, $dump = false) 36 | { 37 | $code = fromFile($file); 38 | $walker = new Walker($code, true); 39 | 40 | $context = $walker->walk(); 41 | 42 | if ($dump === true) { 43 | dd($context); 44 | } elseif ($dump === 'json') { 45 | dd($context->toJson(JSON_PRETTY_PRINT)); 46 | } elseif ($dump === 'array') { 47 | dd($context->toArray()); 48 | } 49 | 50 | return $context->toJson(JSON_PRETTY_PRINT); 51 | } 52 | 53 | test('basic function', function () { 54 | expect(contextResult('basic-function'))->toBe(createContext([ 55 | [ 56 | 'type' => 'methodCall', 57 | 'autocompleting' => true, 58 | 'name' => 'render', 59 | 'class' => null, 60 | 'arguments' => [], 61 | 'children' => [], 62 | ], 63 | ])); 64 | }); 65 | 66 | test('should not parse because of quote is not open', function () { 67 | // TODO: A single " is somehow translated string literal and doesn't work correctly 68 | expect(contextResult('no-parse-closed-string'))->toBe(createContext([])); 69 | }); 70 | 71 | test('basic function with params', function () { 72 | expect(contextResult('basic-function-with-param'))->toBe(createContext([ 73 | [ 74 | 'type' => 'methodCall', 75 | 'autocompleting' => true, 76 | 'name' => 'render', 77 | 'class' => null, 78 | 'arguments' => [ 79 | [ 80 | 'type' => 'string', 81 | 'value' => 'my-view', 82 | ], 83 | ], 84 | 'children' => [], 85 | ], 86 | ])); 87 | }); 88 | 89 | test('basic static method', function () { 90 | expect(contextResult('basic-static-method'))->toBe(createContext([ 91 | [ 92 | 'type' => 'methodCall', 93 | 'autocompleting' => true, 94 | 'name' => 'where', 95 | 'class' => 'App\Models\User', 96 | 'arguments' => [], 97 | 'children' => [], 98 | ], 99 | ])); 100 | }); 101 | 102 | test('basic static method with params', function () { 103 | expect(contextResult('basic-static-method-with-params'))->toBe(createContext([ 104 | [ 105 | 'type' => 'methodCall', 106 | 'autocompleting' => true, 107 | 'name' => 'where', 108 | 'class' => 'App\Models\User', 109 | 'arguments' => [ 110 | [ 111 | 'type' => 'string', 112 | 'value' => 'email', 113 | ], 114 | ], 115 | 'children' => [], 116 | ], 117 | ])); 118 | }); 119 | 120 | test('chained static method with params', function () { 121 | expect(contextResult('chained-static-method-with-params'))->toBe(createContext([ 122 | [ 123 | 'type' => 'methodCall', 124 | 'autocompleting' => true, 125 | 'name' => 'orWhere', 126 | 'class' => 'App\Models\User', 127 | 'arguments' => [ 128 | [ 129 | 'type' => 'string', 130 | 'value' => 'name', 131 | ], 132 | ], 133 | 'children' => [ 134 | [ 135 | 'type' => 'methodCall', 136 | 'name' => 'where', 137 | 'class' => 'App\Models\User', 138 | 'arguments' => [ 139 | [ 140 | 'type' => 'string', 141 | 'value' => 'email', 142 | ], 143 | [ 144 | 'type' => 'string', 145 | 'value' => '', 146 | ], 147 | ], 148 | 'children' => [], 149 | ], 150 | ], 151 | ], 152 | ])); 153 | }); 154 | 155 | test('basic method', function () { 156 | expect(contextResult('basic-method'))->toBe(createContext([ 157 | [ 158 | 'type' => 'assignment', 159 | 'name' => 'user', 160 | 'value' => [ 161 | [ 162 | 'type' => 'object', 163 | 'name' => 'App\Models\User', 164 | 'children' => [], 165 | ], 166 | ], 167 | ], 168 | [ 169 | 'type' => 'methodCall', 170 | 'autocompleting' => true, 171 | 'name' => 'where', 172 | 'class' => 'App\Models\User', 173 | 'arguments' => [], 174 | 'children' => [], 175 | ], 176 | ])); 177 | }); 178 | 179 | test('basic method with params', function () { 180 | expect(contextResult('basic-method-with-params'))->toBe(createContext([ 181 | [ 182 | 'type' => 'assignment', 183 | 'name' => 'user', 184 | 'value' => [ 185 | [ 186 | 'type' => 'object', 187 | 'name' => 'App\Models\User', 188 | 'children' => [], 189 | ], 190 | ], 191 | ], 192 | [ 193 | 'type' => 'methodCall', 194 | 'autocompleting' => true, 195 | 'name' => 'where', 196 | 'class' => 'App\Models\User', 197 | 'arguments' => [ 198 | [ 199 | 'type' => 'string', 200 | 'value' => 'email', 201 | ], 202 | ], 203 | 'children' => [], 204 | ], 205 | ])); 206 | }); 207 | 208 | test('chained method with params', function () { 209 | expect(contextResult('chained-method-with-params'))->toBe(createContext([ 210 | [ 211 | 'type' => 'assignment', 212 | 'name' => 'user', 213 | 'value' => [ 214 | [ 215 | 'type' => 'object', 216 | 'name' => 'App\Models\User', 217 | 'children' => [], 218 | ], 219 | ], 220 | ], 221 | [ 222 | 'type' => 'methodCall', 223 | 'autocompleting' => true, 224 | 'name' => 'orWhere', 225 | 'class' => 'App\Models\User', 226 | 'arguments' => [ 227 | [ 228 | 'type' => 'string', 229 | 'value' => 'name', 230 | ], 231 | ], 232 | 'children' => [ 233 | [ 234 | 'type' => 'methodCall', 235 | 'name' => 'where', 236 | 'class' => 'App\Models\User', 237 | 'arguments' => [ 238 | [ 239 | 'type' => 'string', 240 | 'value' => 'email', 241 | ], 242 | [ 243 | 'type' => 'string', 244 | 'value' => '', 245 | ], 246 | ], 247 | 'children' => [], 248 | ], 249 | ], 250 | ], 251 | ])); 252 | }); 253 | 254 | test('anonymous function as param', function () { 255 | expect(contextResult('anonymous-function-param'))->toBe(createContext([ 256 | [ 257 | 'type' => 'methodCall', 258 | 'autocompleting' => true, 259 | 'name' => 'where', 260 | 'class' => 'App\Models\User', 261 | 'arguments' => [ 262 | [ 263 | 'type' => 'closure', 264 | 'parameters' => [ 265 | [ 266 | 'types' => ['Illuminate\Database\Query\Builder'], 267 | 'name' => 'q', 268 | ], 269 | ], 270 | 'children' => [ 271 | [ 272 | 'type' => 'methodCall', 273 | 'autocompleting' => true, 274 | 'name' => 'whereIn', 275 | 'class' => 'Illuminate\Database\Query\Builder', 276 | 'arguments' => [], 277 | 'children' => [], 278 | ], 279 | ], 280 | ], 281 | ], 282 | 'children' => [], 283 | ], 284 | ])); 285 | }); 286 | 287 | test('arrow function as param', function () { 288 | expect(contextResult('arrow-function-param'))->toBe(createContext([ 289 | [ 290 | 'type' => 'methodCall', 291 | 'autocompleting' => true, 292 | 'name' => 'where', 293 | 'class' => 'App\Models\User', 294 | 'arguments' => [ 295 | [ 296 | 'type' => 'closure', 297 | 'parameters' => [ 298 | [ 299 | 'types' => ['Illuminate\Database\Query\Builder'], 300 | 'name' => 'q', 301 | ], 302 | ], 303 | 'children' => [ 304 | [ 305 | 'type' => 'methodCall', 306 | 'autocompleting' => true, 307 | 'name' => 'whereIn', 308 | 'class' => 'Illuminate\Database\Query\Builder', 309 | 'arguments' => [], 310 | 'children' => [], 311 | ], 312 | ], 313 | ], 314 | ], 315 | 'children' => [], 316 | ], 317 | ])); 318 | }); 319 | 320 | test('nested functions', function () { 321 | expect(contextResult('nested'))->toBe(createContext([ 322 | [ 323 | 'type' => 'methodCall', 324 | 'autocompleting' => true, 325 | 'name' => 'get', 326 | 'class' => 'Route', 327 | 'arguments' => [ 328 | [ 329 | 'type' => 'string', 330 | 'value' => '/', 331 | ], 332 | [ 333 | 'type' => 'closure', 334 | 'parameters' => [], 335 | 'children' => [ 336 | [ 337 | 'type' => 'methodCall', 338 | 'name' => 'trans', 339 | 'class' => null, 340 | 'arguments' => [ 341 | [ 342 | 'type' => 'string', 343 | 'value' => 'auth.throttle', 344 | ], 345 | ], 346 | 'children' => [], 347 | ], 348 | [ 349 | 'type' => 'methodCall', 350 | 'autocompleting' => true, 351 | 'name' => 'where', 352 | 'class' => 'App\Models\User', 353 | 'arguments' => [], 354 | 'children' => [], 355 | ], 356 | ], 357 | ], 358 | ], 359 | 'children' => [], 360 | ], 361 | ])); 362 | }); 363 | 364 | test('array with arrow function', function () { 365 | expect(contextResult('array-with-arrow-function'))->toBe(createContext([ 366 | [ 367 | 'type' => 'methodCall', 368 | 'autocompleting' => true, 369 | 'name' => 'with', 370 | 'class' => 'App\Models\User', 371 | 'arguments' => [ 372 | [ 373 | 'type' => 'array', 374 | 'autocompleting' => true, 375 | 'children' => [ 376 | [ 377 | 'key' => [ 378 | 'type' => 'string', 379 | 'value' => 'team', 380 | ], 381 | 'value' => [ 382 | 'type' => 'closure', 383 | 'parameters' => [ 384 | [ 385 | 'types' => ['Illuminate\Database\Query\Builder'], 386 | 'name' => 'q', 387 | ], 388 | ], 389 | 'children' => [ 390 | [ 391 | 'type' => 'methodCall', 392 | 'autocompleting' => true, 393 | 'name' => 'where', 394 | 'class' => 'Illuminate\Database\Query\Builder', 395 | 'arguments' => [], 396 | 'children' => [], 397 | ], 398 | ], 399 | ], 400 | 'autocompletingValue' => true, 401 | ], 402 | ], 403 | 'autocompletingKey' => false, 404 | 'autocompletingValue' => true, 405 | ], 406 | ], 407 | 'children' => [], 408 | ], 409 | ])); 410 | }); 411 | 412 | test('array with arrow function several keys', function () { 413 | expect(contextResult('array-with-arrow-function-several-keys'))->toBe(createContext([ 414 | [ 415 | 'type' => 'methodCall', 416 | 'autocompleting' => true, 417 | 'name' => 'with', 418 | 'class' => 'App\Models\User', 419 | 'arguments' => [ 420 | [ 421 | 'type' => 'array', 422 | 'autocompleting' => true, 423 | 'children' => [ 424 | [ 425 | 'key' => [ 426 | 'type' => 'string', 427 | 'value' => 'team', 428 | ], 429 | 'value' => [ 430 | 'type' => 'closure', 431 | 'parameters' => [ 432 | [ 433 | 'types' => ['Illuminate\Database\Query\Builder'], 434 | 'name' => 'q', 435 | ], 436 | ], 437 | 'children' => [ 438 | [ 439 | 'type' => 'methodCall', 440 | 'name' => 'where', 441 | 'class' => 'Illuminate\Database\Query\Builder', 442 | 'arguments' => [ 443 | [ 444 | 'type' => 'string', 445 | 'value' => '', 446 | ], 447 | [ 448 | 'type' => 'string', 449 | 'value' => '', 450 | ], 451 | ], 452 | 'children' => [], 453 | ], 454 | ], 455 | ], 456 | ], 457 | [ 458 | 'key' => [ 459 | 'type' => 'string', 460 | 'value' => 'organization', 461 | ], 462 | 'value' => [ 463 | 'type' => 'closure', 464 | 'parameters' => [ 465 | [ 466 | 'types' => [], 467 | 'name' => 'q', 468 | ], 469 | ], 470 | 'children' => [ 471 | [ 472 | 'type' => 'methodCall', 473 | 'autocompleting' => true, 474 | 'name' => 'whereIn', 475 | 'class' => null, 476 | 'arguments' => [], 477 | 'children' => [], 478 | ], 479 | ], 480 | ], 481 | 'autocompletingValue' => true, 482 | ], 483 | ], 484 | 'autocompletingKey' => false, 485 | 'autocompletingValue' => true, 486 | ], 487 | ], 488 | 'children' => [], 489 | ], 490 | ])); 491 | }); 492 | 493 | test('eloquent make from set variable', function () { 494 | expect(contextResult('eloquent-make-from-set-variable'))->toBe(createContext([ 495 | [ 496 | 'type' => 'classDefinition', 497 | 'name' => 'App\Http\Controllers\ProviderController', 498 | 'extends' => 'App\Http\Controllers\Controller', 499 | 'implements' => [], 500 | 'properties' => [], 501 | 'children' => [ 502 | [ 503 | 'type' => 'methodDefinition', 504 | 'name' => 'store', 505 | 'parameters' => [ 506 | [ 507 | 'types' => ['Illuminate\Http\Request'], 508 | 'name' => 'request', 509 | ], 510 | ], 511 | 'children' => [ 512 | [ 513 | 'type' => 'assignment', 514 | 'name' => 'provider', 515 | 'value' => [ 516 | [ 517 | 'type' => 'methodCall', 518 | 'autocompleting' => true, 519 | 'name' => 'make', 520 | 'class' => 'App\Models\Provider', 521 | 'arguments' => [ 522 | [ 523 | 'type' => 'array', 524 | 'autocompleting' => true, 525 | 'children' => [], 526 | 'autocompletingKey' => true, 527 | 'autocompletingValue' => true, 528 | ], 529 | ], 530 | 'children' => [], 531 | ], 532 | ], 533 | ], 534 | ], 535 | ], 536 | ], 537 | ], 538 | ])); 539 | }); 540 | 541 | test('array with arrow function several keys and second param', function () { 542 | expect(contextResult('array-with-arrow-function-several-keys-and-second-param'))->toBe(createContext([ 543 | [ 544 | 'type' => 'methodCall', 545 | 'autocompleting' => true, 546 | 'name' => 'with', 547 | 'class' => 'App\Models\User', 548 | 'arguments' => [ 549 | [ 550 | 'type' => 'array', 551 | 'autocompleting' => true, 552 | 'children' => [ 553 | [ 554 | 'key' => [ 555 | 'type' => 'string', 556 | 'value' => 'team', 557 | ], 558 | 'value' => [ 559 | 'type' => 'closure', 560 | 'parameters' => [ 561 | [ 562 | 'types' => ['Illuminate\Database\Query\Builder'], 563 | 'name' => 'q', 564 | ], 565 | ], 566 | 'children' => [ 567 | [ 568 | 'type' => 'methodCall', 569 | 'name' => 'where', 570 | 'class' => 'Illuminate\Database\Query\Builder', 571 | 'arguments' => [ 572 | [ 573 | 'type' => 'string', 574 | 'value' => '', 575 | ], 576 | [ 577 | 'type' => 'string', 578 | 'value' => '', 579 | ], 580 | ], 581 | 'children' => [], 582 | ], 583 | ], 584 | ], 585 | ], 586 | [ 587 | 'key' => [ 588 | 'type' => 'string', 589 | 'value' => 'organization', 590 | ], 591 | 'value' => [ 592 | 'type' => 'closure', 593 | 'parameters' => [ 594 | [ 595 | 'types' => [], 596 | 'name' => 'q', 597 | ], 598 | ], 599 | 'children' => [ 600 | [ 601 | 'type' => 'methodCall', 602 | 'autocompleting' => true, 603 | 'name' => 'whereIn', 604 | 'class' => null, 605 | 'arguments' => [ 606 | [ 607 | 'type' => 'string', 608 | 'value' => '', 609 | ], 610 | ], 611 | 'children' => [], 612 | ], 613 | ], 614 | ], 615 | 'autocompletingValue' => true, 616 | ], 617 | ], 618 | 'autocompletingKey' => false, 619 | 'autocompletingValue' => true, 620 | ], 621 | ], 622 | 'children' => [], 623 | ], 624 | ])); 625 | }); 626 | 627 | test('array with arrow function missing second key', function () { 628 | expect(contextResult('array-with-arrow-function-missing-second-key'))->toBe(createContext([ 629 | [ 630 | 'type' => 'methodCall', 631 | 'autocompleting' => true, 632 | 'name' => 'with', 633 | 'class' => 'App\Models\User', 634 | 'arguments' => [ 635 | [ 636 | 'type' => 'array', 637 | 'autocompleting' => true, 638 | 'children' => [ 639 | [ 640 | 'key' => [ 641 | 'type' => 'string', 642 | 'value' => 'team', 643 | ], 644 | 'value' => [ 645 | 'type' => 'closure', 646 | 'parameters' => [ 647 | [ 648 | 'types' => ['Illuminate\Database\Query\Builder'], 649 | 'name' => 'q', 650 | ], 651 | ], 652 | 'children' => [ 653 | [ 654 | 'type' => 'methodCall', 655 | 'name' => 'where', 656 | 'class' => 'Illuminate\Database\Query\Builder', 657 | 'arguments' => [ 658 | [ 659 | 'type' => 'string', 660 | 'value' => '', 661 | ], 662 | ], 663 | 'children' => [], 664 | ], 665 | ], 666 | ], 667 | ], 668 | ], 669 | 'autocompletingKey' => true, 670 | 'autocompletingValue' => false, 671 | ], 672 | ], 673 | 'children' => [], 674 | ], 675 | ])); 676 | }); 677 | 678 | test('this reference', function () { 679 | expect(contextResult('this-reference'))->toBe(createContext([ 680 | [ 681 | 'type' => 'classDefinition', 682 | 'name' => 'App\Commands\MyCommand', 683 | 'extends' => 'Vendor\Package\Thing', 684 | 'implements' => ['Vendor\Package\Contracts\BigContract', 'Vendor\Package\Support\Contracts\SmallContract'], 685 | 'properties' => [ 686 | [ 687 | 'types' => ['App\Models\User'], 688 | 'name' => 'user', 689 | ], 690 | ], 691 | 'children' => [ 692 | [ 693 | 'type' => 'methodDefinition', 694 | 'name' => 'render', 695 | 'parameters' => [ 696 | [ 697 | 'types' => ['array'], 698 | 'name' => 'params', 699 | ], 700 | ], 701 | 'children' => [ 702 | [ 703 | 'type' => 'methodCall', 704 | 'autocompleting' => true, 705 | 'name' => 'where', 706 | 'class' => 'App\Models\User', 707 | 'arguments' => [ 708 | [ 709 | 'type' => 'string', 710 | 'value' => 'url', 711 | ], 712 | ], 713 | 'children' => [ 714 | [ 715 | 'type' => 'methodCall', 716 | 'name' => 'user', 717 | 'class' => 'App\Models\User', 718 | 'arguments' => [], 719 | 'children' => [], 720 | ], 721 | ], 722 | ], 723 | ], 724 | ], 725 | ], 726 | ], 727 | ])); 728 | }); 729 | 730 | test('object instantiation')->todo(); 731 | --------------------------------------------------------------------------------