Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

ConfigFilesHelper::setDefaultConfigFile()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
c 1
b 0
f 1
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Backpack\CRUD\app\Console\Commands\Upgrade\Support;
4
5
use Backpack\CRUD\app\Console\Commands\Upgrade\UpgradeContext;
6
7
class ConfigFilesHelper
8
{
9
    protected UpgradeContext $context;
10
    protected string $publishedRoot;
11
    protected string $packageRoot;
12
    protected bool $publishedIsFile = false;
13
    protected bool $packageIsFile = false;
14
    protected ?string $defaultConfigFile = null;
15
    private array $configFiles = [];
16
17
    public function __construct(
18
        UpgradeContext $context,
19
        string $publishedDirectory,
20
        string $packageDirectory
21
    ) {
22
        $this->context = $context;
23
        $this->setPublishedDirectory($publishedDirectory);
24
        $this->setPackageDirectory($packageDirectory);
25
        $this->initializeConfigFiles();
26
    }
27
28
    public function setDefaultConfigFile(string $configFile): void
29
    {
30
        $this->defaultConfigFile = $configFile;
31
    }
32
33
    protected function initializeConfigFiles(): void
34
    {
35
        if ($this->configFiles !== []) {
36
            return;
37
        }
38
39
        if (! isset($this->publishedRoot)) {
40
            return;
41
        }
42
43
        $this->configFiles = $this->inspectConfigFiles([$this->publishedRoot]);
44
    }
45
46
    public function publishedRelativePath(string $path): string
47
    {
48
        $absolutePath = $this->resolvePublishedPath($path);
49
        $relativePath = $this->context->relativePath($absolutePath);
50
51
        if ($relativePath !== $absolutePath) {
52
            return $relativePath;
53
        }
54
55
        return $this->relativePublishedPath($absolutePath);
56
    }
57
58
    public function readPublishedFile(string $path): ?string
59
    {
60
        $absolutePath = $this->resolvePublishedPath($path);
61
62
        if (! is_file($absolutePath)) {
63
            return null;
64
        }
65
66
        return $this->context->readFile($this->context->relativePath($absolutePath));
67
    }
68
69
    public function configKeyHasValue(string $key, mixed $expectedValue, ?string $path = null): bool
70
    {
71
        $target = $this->resolveConfigFileArgument($path);
72
73
        if ($target === null) {
74
            return false;
75
        }
76
77
        $config = $this->loadPublishedConfig($target);
78
79
        if ($config === null) {
80
            return false;
81
        }
82
83
        $current = $this->getConfigValueByKey($config, $key);
84
85
        if ($current === null) {
86
            return false;
87
        }
88
89
        if (is_array($current)) {
90
            if (is_array($expectedValue)) {
91
                foreach ($expectedValue as $value) {
92
                    if (! in_array($value, $current, true)) {
93
                        return false;
94
                    }
95
                }
96
97
                return true;
98
            }
99
100
            return in_array($expectedValue, $current, true);
101
        }
102
103
        return $current === $expectedValue;
104
    }
105
106
    public function writePublishedFile(string $path, string $contents): bool
107
    {
108
        $absolutePath = $this->resolvePublishedPath($path);
109
110
        return $this->context->writeFile($this->context->relativePath($absolutePath), $contents);
111
    }
112
113
    public function updateConfigKeyValue(string $key, mixed $newValue, ?string $path = null): bool
114
    {
115
        $target = $this->resolveConfigFileArgument($path);
116
117
        if ($target === null) {
118
            return false;
119
        }
120
121
        if (is_string($newValue) && $this->configKeyHasValue($key, $newValue, $target)) {
122
            return false;
123
        }
124
125
        $contents = $this->readPublishedFile($target);
126
127
        if ($contents === null) {
128
            return false;
129
        }
130
131
        $changed = false;
132
        $pattern = '/(?P<prefix>(["\"])'.preg_quote($key, '/').'\2\s*=>\s*)(?P<value>(?:[^,\r\n\/]|\/(?!\/))+)(?P<suffix>,?[ \t]*(?:\/\/[^\r\n]*)?)/';
133
134
        $updated = preg_replace_callback(
135
            $pattern,
136
            function (array $matches) use ($newValue, &$changed) {
137
                $existing = trim($matches['value']);
138
                $quote = $existing[0] ?? null;
139
140
                if (is_string($newValue)) {
141
                    $preferredQuote = ($quote === "'" || $quote === '"') ? $quote : "'";
142
                    $replacement = $this->exportStringValue($newValue, $preferredQuote);
143
                } else {
144
                    $replacement = $this->exportValue($newValue);
145
                }
146
147
                if ($replacement === $existing) {
148
                    return $matches[0];
149
                }
150
151
                $changed = true;
152
153
                return $matches['prefix'].$replacement.$matches['suffix'];
154
            },
155
            $contents,
156
            1
157
        );
158
159
        if ($updated === null || ! $changed) {
160
            return false;
161
        }
162
163
        return $this->writePublishedFile($target, $updated);
164
    }
165
166
    public function commentOutConfigValue(string $valueExpression, ?string $path = null): bool
167
    {
168
        $target = $this->resolveConfigFileArgument($path);
169
170
        if ($target === null) {
171
            return false;
172
        }
173
174
        $contents = $this->readPublishedFile($target);
175
176
        if ($contents === null) {
177
            return false;
178
        }
179
180
        $pattern = '~^[\t ]*'.preg_quote($valueExpression, '~').'([\t ]*,?[\t ]*)\r?$~m';
181
182
        $updated = preg_replace_callback(
183
            $pattern,
184
            function (array $matches) use ($valueExpression) {
185
                $position = strpos($matches[0], $valueExpression);
186
                $indentation = $position === false ? '' : substr($matches[0], 0, $position);
187
188
                return $indentation.'// '.$valueExpression.$matches[1];
189
            },
190
            $contents,
191
            1,
192
            $count
193
        );
194
195
        if ($updated === null || $count === 0) {
196
            return false;
197
        }
198
199
        return $this->writePublishedFile($target, $updated);
200
    }
201
202
    public function loadPublishedConfig(string $path): ?array
203
    {
204
        return $this->loadConfigArray($this->resolvePublishedPath($path));
205
    }
206
207
    public function loadPackageConfig(string $path): ?array
208
    {
209
        return $this->loadConfigArray($this->packageConfigPath($path));
210
    }
211
212
    public function analyzeConfigFile(string $path): ?array
213
    {
214
        if (! is_file($path)) {
215
            return null;
216
        }
217
218
        $displayPath = $this->context->relativePath($path);
219
        $relativeWithinPublished = $this->relativePublishedPath($path);
220
221
        $publishedConfig = $this->loadConfigArray($path);
222
        $packageConfig = $this->loadPackageConfigFor($path);
223
224
        if ($publishedConfig === null || $packageConfig === null) {
225
            return [
226
                'filename' => $relativeWithinPublished,
227
                'relative_path' => $displayPath,
228
                'absolute_path' => $path,
229
                'published_config' => $publishedConfig,
230
                'package_config' => $packageConfig,
231
                'missing_keys' => [],
232
                'top_level_missing_keys' => [],
233
            ];
234
        }
235
236
        $packageKeys = $this->flattenKeys($packageConfig);
237
        $publishedKeys = $this->flattenKeys($publishedConfig);
238
239
        return [
240
            'filename' => $relativeWithinPublished,
241
            'relative_path' => $displayPath,
242
            'absolute_path' => $path,
243
            'published_config' => $publishedConfig,
244
            'package_config' => $packageConfig,
245
            'missing_keys' => $this->calculateMissingKeys($packageKeys, $publishedKeys),
246
            'top_level_missing_keys' => $this->calculateTopLevelMissingKeys($packageConfig, $publishedConfig),
247
        ];
248
    }
249
250
    public function getMissingEntries(string $path, array $keys): array
251
    {
252
        if (empty($keys)) {
253
            return [];
254
        }
255
256
        $entries = $this->extractPackageEntries($path);
257
258
        if (empty($entries)) {
259
            return [];
260
        }
261
262
        $result = [];
263
264
        foreach ($keys as $key) {
265
            if (isset($entries[$key])) {
266
                $result[] = $entries[$key];
267
            }
268
        }
269
270
        return $result;
271
    }
272
273
    public function inspectConfigFiles(array $paths): array
274
    {
275
        $files = $this->collectFiles($paths);
276
277
        $checkedAny = false;
278
        $missingKeysPerFile = [];
279
        $topLevelEntriesPerFile = [];
280
        $topLevelMissingKeysPerFile = [];
281
        $collectedEntries = [];
282
        $absoluteMap = [];
283
284
        foreach ($files as $absolutePath) {
285
            $analysis = $this->analyzeConfigFile($absolutePath);
286
287
            if ($analysis === null) {
288
                continue;
289
            }
290
291
            $checkedAny = true;
292
293
            $displayPath = $analysis['relative_path'];
294
            $publishedConfig = $analysis['published_config'];
295
            $packageConfig = $analysis['package_config'];
296
297
            $absoluteMap[$displayPath] = $absolutePath;
298
299
            if ($publishedConfig === null || $packageConfig === null) {
300
                continue;
301
            }
302
303
            $missingKeys = $analysis['missing_keys'] ?? [];
304
305
            if (! empty($missingKeys)) {
306
                $missingKeysPerFile[$displayPath] = $missingKeys;
307
            }
308
309
            $topLevelKeys = $analysis['top_level_missing_keys'] ?? [];
310
311
            if (empty($topLevelKeys)) {
312
                continue;
313
            }
314
315
            $entriesByKey = [];
316
317
            foreach ($this->getMissingEntries($absolutePath, $topLevelKeys) as $entry) {
318
                $key = $entry['key'] ?? null;
319
320
                if ($key === null) {
321
                    continue;
322
                }
323
324
                if (! isset($entriesByKey[$key])) {
325
                    $entriesByKey[$key] = $entry;
326
                }
327
328
                if (! isset($collectedEntries[$key])) {
329
                    $collectedEntries[$key] = $entry;
330
                }
331
            }
332
333
            if (! empty($entriesByKey)) {
334
                $topLevelEntriesPerFile[$displayPath] = array_values($entriesByKey);
335
                $topLevelMissingKeysPerFile[$displayPath] = array_keys($entriesByKey);
336
            }
337
        }
338
339
        return [
340
            'checked_any' => $checkedAny,
341
            'missing_keys_per_file' => $missingKeysPerFile,
342
            'top_level_entries_per_file' => $topLevelEntriesPerFile,
343
            'top_level_missing_keys_per_file' => $topLevelMissingKeysPerFile,
344
            'collected_entries' => $collectedEntries,
345
            'absolute_paths' => $absoluteMap,
346
        ];
347
    }
348
349
    public function configFilesPublished(): bool
350
    {
351
        return (bool) ($this->configFiles['checked_any'] ?? false);
352
    }
353
354
    public function missingKeysPerFile(): array
355
    {
356
        return $this->configFiles['missing_keys_per_file'] ?? [];
357
    }
358
359
    public function topLevelEntriesPerFile(): array
360
    {
361
        return $this->configFiles['top_level_entries_per_file'] ?? [];
362
    }
363
364
    public function topLevelMissingKeysPerFile(): array
365
    {
366
        return $this->configFiles['top_level_missing_keys_per_file'] ?? [];
367
    }
368
369
    public function collectedEntries(): array
370
    {
371
        return $this->configFiles['collected_entries'] ?? [];
372
    }
373
374
    public function absolutePaths(): array
375
    {
376
        return $this->configFiles['absolute_paths'] ?? [];
377
    }
378
379
    public function loadConfigArray(string $path): ?array
380
    {
381
        if (! is_file($path)) {
382
            return null;
383
        }
384
385
        $data = include $path;
386
387
        return is_array($data) ? $data : null;
388
    }
389
390
    public function flattenKeys(array $config, string $prefix = ''): array
391
    {
392
        $keys = [];
393
394
        foreach ($config as $key => $value) {
395
            if (is_int($key)) {
396
                if (is_array($value)) {
397
                    $keys = array_merge($keys, $this->flattenKeys($value, $prefix));
398
                }
399
400
                continue;
401
            }
402
403
            $key = (string) $key;
404
            $fullKey = $prefix === '' ? $key : $prefix.'.'.$key;
405
            $keys[] = $fullKey;
406
407
            if (is_array($value)) {
408
                $keys = array_merge($keys, $this->flattenKeys($value, $fullKey));
409
            }
410
        }
411
412
        return array_values(array_unique($keys));
413
    }
414
415
    public function publishedFileContainsKey(string $path, string $key): bool
416
    {
417
        $config = $this->loadPublishedConfig($path);
418
419
        if ($config === null) {
420
            return false;
421
        }
422
423
        return in_array($key, $this->flattenKeys($config), true);
424
    }
425
426
    public function packageConfigPath(string $path): string
427
    {
428
        return $this->packagePathFor($path);
429
    }
430
431
    public function setPublishedDirectory(string $publishedDirectory): void
432
    {
433
        $this->publishedRoot = $this->trimTrailingSeparators(trim($publishedDirectory));
434
435
        if ($this->publishedRoot === '') {
436
            throw new \InvalidArgumentException('Published directory path must not be empty.');
437
        }
438
439
        $this->publishedIsFile = ! is_dir($this->publishedRoot);
440
        $this->configFiles = [];
441
    }
442
443
    public function setPackageDirectory(string $packageDirectory): void
444
    {
445
        $this->packageRoot = $this->trimTrailingSeparators(trim($packageDirectory));
446
447
        if ($this->packageRoot === '') {
448
            throw new \InvalidArgumentException('Package directory path must not be empty.');
449
        }
450
451
        $this->packageIsFile = ! is_dir($this->packageRoot);
452
        $this->configFiles = [];
453
    }
454
455
    public function getPackageEntries(string $path, ?array $onlyKeys = null): array
456
    {
457
        $entries = $this->extractPackageEntries($path);
458
459
        if ($onlyKeys === null) {
460
            return $entries;
461
        }
462
463
        if ($onlyKeys === []) {
464
            return [];
465
        }
466
467
        $lookup = array_fill_keys($onlyKeys, true);
468
469
        return array_intersect_key($entries, $lookup);
470
    }
471
472
    public function getPackageEntry(string $path, string $key): ?array
473
    {
474
        $entries = $this->extractPackageEntries($path);
475
476
        return $entries[$key] ?? null;
477
    }
478
479
    public function buildSnippet(array $entries): string
480
    {
481
        $blocks = [];
482
483
        foreach ($entries as $entry) {
484
            $commentLines = $entry['commentLines'] ?? [];
485
            $entryLines = $entry['entryLines'] ?? [];
486
487
            if (empty($entryLines)) {
488
                continue;
489
            }
490
491
            $normalized = [];
492
493
            foreach ($commentLines as $line) {
494
                $normalized[] = rtrim($line, "\r");
495
            }
496
497
            foreach ($entryLines as $line) {
498
                $normalized[] = rtrim($line, "\r");
499
            }
500
501
            if (! empty($normalized)) {
502
                $blocks[] = implode(PHP_EOL, $normalized);
503
            }
504
        }
505
506
        if (empty($blocks)) {
507
            return '';
508
        }
509
510
        return implode(PHP_EOL.PHP_EOL, $blocks).PHP_EOL;
511
    }
512
513
    public function addEntriesToPublishedFile(string $path, array $entries, ?string &$error = null): bool
514
    {
515
        if (empty($entries)) {
516
            return true;
517
        }
518
519
        $contents = $this->readPublishedFile($path);
520
521
        if ($contents === null) {
522
            $error = sprintf('Could not read %s to update missing configuration keys.', $this->publishedRelativePath($path));
523
524
            return false;
525
        }
526
527
        $snippet = $this->buildSnippet($entries);
528
529
        if ($snippet === '') {
530
            return true;
531
        }
532
533
        $closingPosition = $this->findConfigArrayClosurePosition($contents);
534
535
        if ($closingPosition === null) {
536
            $error = sprintf('Could not locate the end of the configuration array in %s.', $this->publishedRelativePath($path));
537
538
            return false;
539
        }
540
541
        $before = substr($contents, 0, $closingPosition);
542
        $after = substr($contents, $closingPosition);
543
544
        $configArray = $this->loadPublishedConfig($path);
545
        $shouldEnsureTrailingComma = $configArray === null ? true : ! empty($configArray);
546
547
        if ($shouldEnsureTrailingComma) {
548
            $before = $this->ensureTrailingComma($before);
549
        }
550
551
        $newline = str_contains($contents, "\r\n") ? "\r\n" : "\n";
552
        $before = rtrim($before).$newline.$newline;
553
554
        if ($newline !== PHP_EOL) {
555
            $snippet = $this->normalizeNewlines($snippet, $newline);
556
        }
557
558
        $updated = $before.$snippet.$after;
559
560
        if (! $this->writePublishedFile($path, $updated)) {
561
            $error = sprintf('Could not update %s automatically.', $this->publishedRelativePath($path));
562
563
            return false;
564
        }
565
566
        return true;
567
    }
568
569
    public function addKeyToConfigFile(string $path, string $snippet, ?string &$error = null): bool
570
    {
571
        $contents = $this->readPublishedFile($path);
572
573
        if ($contents === null) {
574
            $error = sprintf('Could not read %s to update the configuration keys.', $this->publishedRelativePath($path));
575
576
            return false;
577
        }
578
579
        $newline = str_contains($contents, "\r\n") ? "\r\n" : "\n";
580
        $normalizedSnippet = rtrim($this->normalizeNewlines($snippet, $newline));
581
582
        if ($normalizedSnippet === '') {
583
            return true;
584
        }
585
586
        $normalizedSnippet .= $newline.$newline;
587
        $pattern = '/(return\s*\[\s*(?:\r?\n))/';
588
        $replacement = '$1'.$normalizedSnippet;
589
590
        $updatedContents = preg_replace($pattern, $replacement, $contents, 1, $replacements);
591
592
        if ($updatedContents === null || $replacements === 0) {
593
            $error = sprintf('Could not locate the start of the configuration array in %s.', $this->publishedRelativePath($path));
594
595
            return false;
596
        }
597
598
        if (! $this->writePublishedFile($path, $updatedContents)) {
599
            $error = sprintf('Could not save the updated %s configuration.', $this->publishedRelativePath($path));
600
601
            return false;
602
        }
603
604
        return true;
605
    }
606
607
    public function ensureTrailingComma(string $before): string
608
    {
609
        $trimmed = rtrim($before);
610
611
        if ($trimmed === '') {
612
            return $before;
613
        }
614
615
        $trailingLength = strlen($before) - strlen($trimmed);
616
        $trailingWhitespace = $trailingLength > 0 ? substr($before, -$trailingLength) : '';
617
        $core = $trailingLength > 0 ? substr($before, 0, -$trailingLength) : $before;
618
619
        $parts = preg_split('/(\r\n|\n|\r)/', $core, -1, PREG_SPLIT_DELIM_CAPTURE);
620
621
        if ($parts === false) {
622
            return $before;
623
        }
624
625
        $segments = [];
626
627
        for ($i = 0, $partCount = count($parts); $i < $partCount; $i += 2) {
628
            $segments[] = [
629
                'line' => $parts[$i],
630
                'newline' => $parts[$i + 1] ?? '',
631
            ];
632
        }
633
634
        $modified = false;
635
636
        for ($i = count($segments) - 1; $i >= 0; $i--) {
637
            $line = $segments[$i]['line'];
638
            $trimmedLine = trim($line);
639
640
            if ($trimmedLine === '') {
641
                continue;
642
            }
643
644
            if ($this->isCommentLine($trimmedLine) || $this->isBlockCommentStart($trimmedLine) || str_starts_with($trimmedLine, '*')) {
645
                continue;
646
            }
647
648
            $commentPos = $this->findInlineCommentPosition($line);
649
            $lineBeforeComment = $commentPos !== null ? substr($line, 0, $commentPos) : $line;
650
            $lineBeforeCommentTrimmed = rtrim($lineBeforeComment);
651
652
            if ($lineBeforeCommentTrimmed === '' || $this->looksLikeArrayOpening($lineBeforeCommentTrimmed)) {
653
                continue;
654
            }
655
656
            if (str_ends_with($lineBeforeCommentTrimmed, ',')) {
657
                break;
658
            }
659
660
            if ($commentPos !== null) {
661
                $codeSegment = substr($line, 0, $commentPos);
662
                $commentPart = substr($line, $commentPos);
663
                $codePart = rtrim($codeSegment);
664
                $spaceLength = strlen($codeSegment) - strlen($codePart);
665
                $spaceSegment = $spaceLength > 0 ? substr($codeSegment, -$spaceLength) : '';
666
                $segments[$i]['line'] = $codePart.','.$spaceSegment.$commentPart;
667
            } else {
668
                $segments[$i]['line'] = rtrim($line).',';
669
            }
670
671
            $modified = true;
672
            break;
673
        }
674
675
        if (! $modified) {
676
            return $before;
677
        }
678
679
        $reconstructed = '';
680
681
        foreach ($segments as $segment) {
682
            $reconstructed .= $segment['line'].$segment['newline'];
683
        }
684
685
        return $reconstructed.$trailingWhitespace;
686
    }
687
688
    protected function extractPackageEntries(string $path): array
689
    {
690
        $packagePath = $this->packagePathFor($path);
691
692
        if (! is_file($packagePath)) {
693
            return [];
694
        }
695
696
        $lines = file($packagePath, FILE_IGNORE_NEW_LINES);
697
698
        if ($lines === false) {
699
            return [];
700
        }
701
702
        $entries = [];
703
        $pendingComments = [];
704
        $lineCount = count($lines);
705
        $index = 0;
706
        $withinConfigArray = false;
707
708
        while ($index < $lineCount) {
709
            $line = rtrim($lines[$index], "\r");
710
            $trimmed = ltrim($line);
711
712
            if (! $withinConfigArray) {
713
                if (preg_match('/return\s*\[/', $trimmed)) {
714
                    $withinConfigArray = true;
715
                }
716
717
                $index++;
718
                continue;
719
            }
720
721
            if ($trimmed === '];') {
722
                break;
723
            }
724
725
            if ($this->isBlockCommentStart($trimmed)) {
726
                $pendingComments[] = $line;
727
728
                while (! str_contains($trimmed, '*/') && $index + 1 < $lineCount) {
729
                    $index++;
730
                    $line = rtrim($lines[$index], "\r");
731
                    $trimmed = ltrim($line);
732
                    $pendingComments[] = $line;
733
                }
734
735
                $index++;
736
                continue;
737
            }
738
739
            if ($this->isCommentLine($trimmed)) {
740
                $pendingComments[] = $line;
741
                $index++;
742
                continue;
743
            }
744
745
            if ($trimmed === '' && ! empty($pendingComments)) {
746
                $pendingComments[] = $line;
747
                $index++;
748
                continue;
749
            }
750
751
            if (! preg_match('/^([\'"])\s*(.+?)\1\s*=>/', $trimmed, $matches)) {
752
                $pendingComments = [];
753
                $index++;
754
                continue;
755
            }
756
757
            $key = $matches[2];
758
            $entryLines = [$line];
759
760
            $valueDepth = $this->calculateBracketDelta($line);
761
            $hasTerminatingComma = $this->lineHasTerminatingComma($line);
762
763
            while (($valueDepth > 0 || ! $hasTerminatingComma) && $index + 1 < $lineCount) {
764
                $index++;
765
                $line = rtrim($lines[$index], "\r");
766
                $entryLines[] = $line;
767
768
                $valueDepth += $this->calculateBracketDelta($line);
769
                $hasTerminatingComma = $this->lineHasTerminatingComma($line);
770
            }
771
772
            $entries[$key] = [
773
                'key' => $key,
774
                'commentLines' => $pendingComments,
775
                'entryLines' => $entryLines,
776
            ];
777
778
            $pendingComments = [];
779
            $index++;
780
        }
781
782
        return $entries;
783
    }
784
785
    protected function collectFiles(array $definitions): array
786
    {
787
        $files = [];
788
789
        foreach ($definitions as $definition) {
790
            if (! is_string($definition)) {
791
                continue;
792
            }
793
794
            $path = trim($definition);
795
796
            if ($path === '') {
797
                continue;
798
            }
799
800
            if (is_dir($path)) {
801
                $iterator = new \RecursiveIteratorIterator(
802
                    new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
803
                );
804
805
                foreach ($iterator as $fileInfo) {
806
                    if ($fileInfo->isFile() && strtolower($fileInfo->getExtension()) === 'php') {
807
                        $absolute = realpath($fileInfo->getPathname()) ?: $fileInfo->getPathname();
808
                        $files[$absolute] = true;
809
                    }
810
                }
811
812
                continue;
813
            }
814
815
            if (is_file($path)) {
816
                $absolute = realpath($path) ?: $path;
817
                $files[$absolute] = true;
818
            }
819
        }
820
821
        $paths = array_keys($files);
822
        sort($paths);
823
824
        return $paths;
825
    }
826
827
    protected function loadPackageConfigFor(string $absolutePublishedPath): ?array
828
    {
829
        return $this->loadConfigArray($this->packagePathFor($absolutePublishedPath));
830
    }
831
832
    protected function packagePathFor(string $publishedPath): string
833
    {
834
        $publishedAbsolute = $this->resolvePublishedPath($publishedPath);
835
836
        if ($this->packageRoot === '') {
837
            return $publishedAbsolute;
838
        }
839
840
        if ($this->packageIsFile) {
841
            return $this->packageRoot;
842
        }
843
844
        $packageRoot = rtrim($this->packageRoot, '\/');
845
846
        if ($this->publishedIsFile) {
847
            return $packageRoot.DIRECTORY_SEPARATOR.basename($publishedAbsolute);
848
        }
849
850
        $publishedRoot = rtrim($this->publishedRoot, '\/');
851
852
        if ($publishedRoot !== '') {
853
            $normalizedRoot = $this->normalizePath($publishedRoot);
854
            $normalizedPath = $this->normalizePath($publishedAbsolute);
855
856
            if ($normalizedPath === $normalizedRoot) {
857
                return $packageRoot;
858
            }
859
860
            $prefix = $normalizedRoot.'/';
861
862
            if (str_starts_with($normalizedPath, $prefix)) {
863
                $relative = substr($normalizedPath, strlen($prefix));
864
865
                return $packageRoot.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $relative);
866
            }
867
        }
868
869
        return $packageRoot.DIRECTORY_SEPARATOR.basename($publishedAbsolute);
870
    }
871
872
    protected function trimTrailingSeparators(string $path): string
873
    {
874
        if ($path === '') {
875
            return '';
876
        }
877
878
        if ($path === DIRECTORY_SEPARATOR) {
879
            return $path;
880
        }
881
882
        $trimmed = rtrim($path, '/\\');
883
884
        return $trimmed === '' ? $path : $trimmed;
885
    }
886
887
    protected function resolvePublishedPath(?string $path = null): string
888
    {
889
        $target = $path ?? '';
890
891
        if ($target !== '' && $this->isAbsolutePath($target)) {
892
            return $target;
893
        }
894
895
        if ($this->publishedRoot === '') {
896
            return $target;
897
        }
898
899
        if ($this->publishedIsFile) {
900
            return $this->publishedRoot;
901
        }
902
903
        $relative = $this->trimLeadingSeparators($target);
904
905
        if ($relative === '') {
906
            return $this->publishedRoot;
907
        }
908
909
        return $this->publishedRoot.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $relative);
910
    }
911
912
    protected function trimLeadingSeparators(string $path): string
913
    {
914
        return ltrim($path, '\\/');
915
    }
916
917
    protected function isAbsolutePath(string $path): bool
918
    {
919
        if ($path === '') {
920
            return false;
921
        }
922
923
        if (str_starts_with($path, '/') || str_starts_with($path, '\\')) {
924
            return true;
925
        }
926
927
        return (bool) preg_match('/^[A-Za-z]:[\\\\\/]/', $path);
928
    }
929
930
    protected function relativePublishedPath(string $path): string
931
    {
932
        $absolutePath = $this->resolvePublishedPath($path);
933
934
        if ($this->publishedRoot === '' || $this->publishedIsFile) {
935
            return basename($absolutePath);
936
        }
937
938
        $normalizedRoot = $this->normalizePath($this->publishedRoot);
939
        $normalizedPath = $this->normalizePath($absolutePath);
940
941
        if ($normalizedPath === $normalizedRoot) {
942
            return basename($absolutePath);
943
        }
944
945
        $prefix = $normalizedRoot.'/';
946
947
        if (str_starts_with($normalizedPath, $prefix)) {
948
            return substr($normalizedPath, strlen($prefix));
949
        }
950
951
        return basename($absolutePath);
952
    }
953
954
    protected function normalizePath(string $path): string
955
    {
956
        return rtrim(str_replace('\\', '/', $path), '/');
957
    }
958
959
    protected function resolveConfigFileArgument(?string $path): ?string
960
    {
961
        if ($path !== null) {
962
            return $path;
963
        }
964
965
        return $this->defaultConfigFile;
966
    }
967
968
    protected function getConfigValueByKey(array $config, string $key): mixed
969
    {
970
        if ($key === '') {
971
            return null;
972
        }
973
974
        $segments = explode('.', $key);
975
        $current = $config;
976
977
        foreach ($segments as $segment) {
978
            if (! is_array($current) || ! array_key_exists($segment, $current)) {
979
                return null;
980
            }
981
982
            $current = $current[$segment];
983
        }
984
985
        return $current;
986
    }
987
988
    protected function exportStringValue(string $value, string $quote): string
989
    {
990
        if ($quote === '"') {
991
            return '"'.addcslashes($value, '\\"$').'"';
992
        }
993
994
        return '\''.addcslashes($value, "\\'").'\'';
995
    }
996
997
    protected function exportValue(mixed $value): string
998
    {
999
        return var_export($value, true);
1000
    }
1001
1002
    protected function calculateMissingKeys(array $packageKeys, array $publishedKeys): array
1003
    {
1004
        $missingKeys = array_values(array_diff($packageKeys, $publishedKeys));
1005
        sort($missingKeys);
1006
1007
        return $missingKeys;
1008
    }
1009
1010
    protected function calculateTopLevelMissingKeys(array $packageConfig, array $publishedConfig): array
1011
    {
1012
        $topLevelMissing = array_keys(array_diff_key($packageConfig, $publishedConfig));
1013
        sort($topLevelMissing);
1014
1015
        return $topLevelMissing;
1016
    }
1017
1018
    protected function findConfigArrayClosurePosition(string $contents): ?int
1019
    {
1020
        $position = strrpos($contents, '];');
1021
1022
        return $position === false ? null : $position;
1023
    }
1024
1025
    protected function normalizeNewlines(string $text, string $newline): string
1026
    {
1027
        if ($newline === "\n") {
1028
            return str_replace(["\r\n", "\r"], "\n", $text);
1029
        }
1030
1031
        if ($newline === "\r\n") {
1032
            $normalized = str_replace(["\r\n", "\r"], "\n", $text);
1033
1034
            return str_replace("\n", "\r\n", $normalized);
1035
        }
1036
1037
        return $text;
1038
    }
1039
1040
    protected function stripStrings(string $line): string
1041
    {
1042
        $result = '';
1043
        $length = strlen($line);
1044
        $inSingle = false;
1045
        $inDouble = false;
1046
1047
        for ($i = 0; $i < $length; $i++) {
1048
            $char = $line[$i];
1049
1050
            if ($char === '\'' && ! $inDouble && ! $this->isCharacterEscaped($line, $i)) {
1051
                $inSingle = ! $inSingle;
0 ignored issues
show
introduced by
The condition $inSingle is always false.
Loading history...
1052
                continue;
1053
            }
1054
1055
            if ($char === '"' && ! $inSingle && ! $this->isCharacterEscaped($line, $i)) {
1056
                $inDouble = ! $inDouble;
1057
                continue;
1058
            }
1059
1060
            if ($inSingle || $inDouble) {
1061
                if ($char === '\\' && $i + 1 < $length) {
1062
                    $i++;
1063
                }
1064
1065
                continue;
1066
            }
1067
1068
            $result .= $char;
1069
        }
1070
1071
        return $result;
1072
    }
1073
1074
    protected function stripInlineComment(string $line): string
1075
    {
1076
        $clean = $this->stripStrings($line);
1077
        $parts = explode('//', $clean, 2);
1078
1079
        return $parts[0];
1080
    }
1081
1082
    protected function findInlineCommentPosition(string $line): ?int
1083
    {
1084
        $length = strlen($line);
1085
        $inSingle = false;
1086
        $inDouble = false;
1087
1088
        for ($i = 0; $i < $length; $i++) {
1089
            $char = $line[$i];
1090
1091
            if ($char === '\'' && ! $inDouble) {
1092
                if (! $this->isCharacterEscaped($line, $i)) {
1093
                    $inSingle = ! $inSingle;
0 ignored issues
show
introduced by
The condition $inSingle is always false.
Loading history...
1094
                }
1095
1096
                continue;
1097
            }
1098
1099
            if ($char === '"' && ! $inSingle) {
1100
                if (! $this->isCharacterEscaped($line, $i)) {
1101
                    $inDouble = ! $inDouble;
1102
                }
1103
1104
                continue;
1105
            }
1106
1107
            if ($inSingle || $inDouble) {
1108
                continue;
1109
            }
1110
1111
            if ($char === '/' && $i + 1 < $length && $line[$i + 1] === '/') {
1112
                return $i;
1113
            }
1114
        }
1115
1116
        return null;
1117
    }
1118
1119
    protected function isCharacterEscaped(string $line, int $position): bool
1120
    {
1121
        $escapeCount = 0;
1122
1123
        for ($i = $position - 1; $i >= 0 && $line[$i] === '\\'; $i--) {
1124
            $escapeCount++;
1125
        }
1126
1127
        return $escapeCount % 2 === 1;
1128
    }
1129
1130
    protected function looksLikeArrayOpening(string $line): bool
1131
    {
1132
        if ($line === '[' || $line === '(') {
1133
            return true;
1134
        }
1135
1136
        if (preg_match('/^(return\s+)?\[\s*$/i', $line)) {
1137
            return true;
1138
        }
1139
1140
        if (preg_match('/^(return\s+)?array\s*\(\s*$/i', $line)) {
1141
            return true;
1142
        }
1143
1144
        return false;
1145
    }
1146
1147
    protected function calculateBracketDelta(string $line): int
1148
    {
1149
        $clean = $this->stripStrings($line);
1150
1151
        $delta = substr_count($clean, '[') - substr_count($clean, ']');
1152
        $delta += substr_count($clean, '(') - substr_count($clean, ')');
1153
        $delta += substr_count($clean, '{') - substr_count($clean, '}');
1154
1155
        return $delta;
1156
    }
1157
1158
    protected function lineHasTerminatingComma(string $line): bool
1159
    {
1160
        $clean = $this->stripInlineComment($line);
1161
1162
        return str_contains($clean, ',');
1163
    }
1164
1165
    protected function isCommentLine(string $trimmedLine): bool
1166
    {
1167
        return str_starts_with($trimmedLine, '//');
1168
    }
1169
1170
    protected function isBlockCommentStart(string $trimmedLine): bool
1171
    {
1172
        return str_starts_with($trimmedLine, '/*');
1173
    }
1174
}
1175