├── .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 |
--------------------------------------------------------------------------------