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

Passed
Push — add-update-command ( 1611a2...0fa6fa )
by Pedro
13:26
created

ConfigFilesHelper::loadConfigArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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