Passed
Pull Request — master (#315)
by Théo
02:33
created

retrievePhpCompactorIgnoredAnnotations()   B

Complexity

Conditions 10
Paths 22

Size

Total Lines 59
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 59
rs 7.6666
c 0
b 0
f 0
cc 10
nc 22
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use Assert\Assertion;
18
use Closure;
19
use DateTimeImmutable;
20
use DateTimeZone;
21
use Herrera\Box\Compactor\Php as LegacyPhp;
22
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
23
use Humbug\PhpScoper\Console\ApplicationFactory;
24
use Humbug\PhpScoper\Scoper;
25
use Humbug\PhpScoper\Scoper\FileWhitelistScoper;
26
use InvalidArgumentException;
27
use KevinGH\Box\Annotation\AnnotationDumper;
28
use KevinGH\Box\Annotation\DocblockAnnotationParser;
29
use KevinGH\Box\Annotation\DocblockParser;
30
use KevinGH\Box\Compactor\Php as PhpCompactor;
31
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
32
use KevinGH\Box\Composer\ComposerConfiguration;
33
use KevinGH\Box\Json\Json;
34
use KevinGH\Box\PhpScoper\SimpleScoper;
35
use Phar;
36
use RuntimeException;
37
use Seld\JsonLint\ParsingException;
38
use SplFileInfo;
39
use stdClass;
40
use Symfony\Component\Finder\Finder;
41
use Symfony\Component\Process\Process;
42
use const E_USER_DEPRECATED;
43
use function array_column;
44
use function array_diff;
45
use function array_filter;
46
use function array_flip;
47
use function array_key_exists;
48
use function array_keys;
49
use function array_map;
50
use function array_merge;
51
use function array_unique;
52
use function array_values;
53
use function array_walk;
54
use function constant;
55
use function current;
56
use function defined;
57
use function dirname;
58
use function explode;
59
use function file_exists;
60
use function getcwd;
61
use function implode;
62
use function in_array;
63
use function intval;
64
use function is_array;
65
use function is_bool;
66
use function is_file;
67
use function is_link;
68
use function is_object;
69
use function is_readable;
70
use function is_string;
71
use function iter\map;
72
use function iter\toArray;
73
use function iter\values;
74
use function KevinGH\Box\FileSystem\canonicalize;
75
use function KevinGH\Box\FileSystem\file_contents;
76
use function KevinGH\Box\FileSystem\is_absolute_path;
77
use function KevinGH\Box\FileSystem\longest_common_base_path;
78
use function KevinGH\Box\FileSystem\make_path_absolute;
79
use function KevinGH\Box\FileSystem\make_path_relative;
80
use function krsort;
81
use function preg_match;
82
use function preg_replace;
83
use function property_exists;
84
use function realpath;
85
use function sprintf;
86
use function strtoupper;
87
use function substr;
88
use function trigger_error;
89
use function trim;
90
91
/**
92
 * @private
93
 */
94
final class Configuration
95
{
96
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
97
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
98
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
99
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
100
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
101
    private const DEFAULT_BANNER = <<<'BANNER'
102
Generated by Humbug Box.
103
104
@link https://github.com/humbug/box
105
BANNER;
106
    private const FILES_SETTINGS = [
107
        'directories',
108
        'finder',
109
    ];
110
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
111
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
112
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
113
114
    private const DEFAULT_IGNORED_ANNOTATIONS = [
115
        'abstract',
116
        'access',
117
        'annotation',
118
        'api',
119
        'attribute',
120
        'attributes',
121
        'author',
122
        'category',
123
        'code',
124
        'codecoverageignore',
125
        'codecoverageignoreend',
126
        'codecoverageignorestart',
127
        'copyright',
128
        'deprec',
129
        'deprecated',
130
        'endcode',
131
        'example',
132
        'exception',
133
        'filesource',
134
        'final',
135
        'fixme',
136
        'global',
137
        'ignore',
138
        'ingroup',
139
        'inheritdoc',
140
        'internal',
141
        'license',
142
        'link',
143
        'magic',
144
        'method',
145
        'name',
146
        'override',
147
        'package',
148
        'package_version',
149
        'param',
150
        'private',
151
        'property',
152
        'required',
153
        'return',
154
        'see',
155
        'since',
156
        'static',
157
        'staticvar',
158
        'subpackage',
159
        'suppresswarnings',
160
        'target',
161
        'throw',
162
        'throws',
163
        'todo',
164
        'tutorial',
165
        'usedby',
166
        'uses',
167
        'var',
168
        'version',
169
    ];
170
171
    private const ALGORITHM_KEY = 'algorithm';
172
    private const ALIAS_KEY = 'alias';
173
    private const ANNOTATIONS_KEY = 'annotations';
174
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
175
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
176
    private const BANNER_KEY = 'banner';
177
    private const BANNER_FILE_KEY = 'banner-file';
178
    private const BASE_PATH_KEY = 'base-path';
179
    private const BLACKLIST_KEY = 'blacklist';
180
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
181
    private const CHMOD_KEY = 'chmod';
182
    private const COMPACTORS_KEY = 'compactors';
183
    private const COMPRESSION_KEY = 'compression';
184
    private const DATETIME_KEY = 'datetime';
185
    private const DATETIME_FORMAT_KEY = 'datetime-format';
186
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
187
    private const DIRECTORIES_KEY = 'directories';
188
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
189
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
190
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
191
    private const FILES_KEY = 'files';
192
    private const FILES_BIN_KEY = 'files-bin';
193
    private const FINDER_KEY = 'finder';
194
    private const FINDER_BIN_KEY = 'finder-bin';
195
    private const GIT_KEY = 'git';
196
    private const GIT_COMMIT_KEY = 'git-commit';
197
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
198
    private const GIT_TAG_KEY = 'git-tag';
199
    private const GIT_VERSION_KEY = 'git-version';
200
    private const INTERCEPT_KEY = 'intercept';
201
    private const KEY_KEY = 'key';
202
    private const KEY_PASS_KEY = 'key-pass';
203
    private const MAIN_KEY = 'main';
204
    private const MAP_KEY = 'map';
205
    private const METADATA_KEY = 'metadata';
206
    private const OUTPUT_KEY = 'output';
207
    private const PHP_SCOPER_KEY = 'php-scoper';
208
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
209
    private const REPLACEMENTS_KEY = 'replacements';
210
    private const SHEBANG_KEY = 'shebang';
211
    private const STUB_KEY = 'stub';
212
213
    private $file;
214
    private $fileMode;
215
    private $alias;
216
    private $basePath;
217
    private $composerJson;
218
    private $composerLock;
219
    private $files;
220
    private $binaryFiles;
221
    private $autodiscoveredFiles;
222
    private $dumpAutoload;
223
    private $excludeComposerFiles;
224
    private $compactors;
225
    private $compressionAlgorithm;
226
    private $mainScriptPath;
227
    private $mainScriptContents;
228
    private $fileMapper;
229
    private $metadata;
230
    private $tmpOutputPath;
231
    private $outputPath;
232
    private $privateKeyPassphrase;
233
    private $privateKeyPath;
234
    private $promptForPrivateKey;
235
    private $processedReplacements;
236
    private $shebang;
237
    private $signingAlgorithm;
238
    private $stubBannerContents;
239
    private $stubBannerPath;
240
    private $stubPath;
241
    private $isInterceptFileFuncs;
242
    private $isStubGenerated;
243
    private $checkRequirements;
244
    private $warnings;
245
    private $recommendations;
246
247
    public static function create(?string $file, stdClass $raw): self
248
    {
249
        $logger = new ConfigurationLogger();
250
251
        $basePath = self::retrieveBasePath($file, $raw, $logger);
252
253
        $composerFiles = self::retrieveComposerFiles($basePath);
254
255
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
256
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
257
258
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
259
260
        /** @var (string|null)[] $composerJson */
261
        $composerJson = $composerFiles[0];
262
        /** @var (string|null)[] $composerJson */
263
        $composerLock = $composerFiles[1];
264
265
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
266
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
267
268
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
269
270
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
271
272
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
273
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
274
275
        if (null !== $stubBannerPath) {
276
            $stubBannerContents = file_contents($stubBannerPath);
277
        }
278
279
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
280
281
        if (null !== $stubBannerPath && self::DEFAULT_BANNER === $stubBannerContents) {
282
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
283
        }
284
285
        $isInterceptsFileFuncs = self::retrieveInterceptsFileFuncs($raw, $isStubGenerated, $logger);
286
287
        $checkRequirements = self::retrieveCheckRequirements(
288
            $raw,
289
            null !== $composerJson[0],
290
            null !== $composerLock[0],
291
            false === $isStubGenerated && null === $stubPath,
292
            $logger
293
        );
294
295
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
0 ignored issues
show
Bug introduced by
It seems like $composerJson[1] can also be of type string; however, parameter $composerJsonDecodedContents of KevinGH\Box\Composer\Com...::retrieveDevPackages() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

295
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, /** @scrutinizer ignore-type */ $composerJson[1], $composerLock[1]);
Loading history...
296
297
        /**
298
         * @var string[]
299
         * @var Closure  $blacklistFilter
300
         */
301
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
302
            $raw,
303
            $basePath,
304
            $logger,
305
            $tmpOutputPath,
306
            $outputPath,
307
            $mainScriptPath
308
        );
309
310
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
311
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
312
313
        $filesAggregate = self::collectFiles(
314
            $raw,
315
            $basePath,
316
            $mainScriptPath,
317
            $blacklistFilter,
318
            $excludedPaths,
319
            $devPackages,
320
            $composerFiles,
321
            $composerJson,
322
            $autodiscoverFiles,
323
            $forceFilesAutodiscovery,
324
            $logger
325
        );
326
        $binaryFilesAggregate = self::collectBinaryFiles(
327
            $raw,
328
            $basePath,
329
            $mainScriptPath,
330
            $blacklistFilter,
331
            $excludedPaths,
332
            $devPackages,
333
            $logger
334
        );
335
336
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
337
338
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
339
340
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
341
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
342
343
        $fileMode = self::retrieveFileMode($raw, $logger);
344
345
        $map = self::retrieveMap($raw, $logger);
346
        $fileMapper = new MapFile($basePath, $map);
347
348
        $metadata = self::retrieveMetadata($raw, $logger);
349
350
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
351
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
352
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
353
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
354
355
        $replacements = self::retrieveReplacements($raw, $file, $logger);
356
357
        return new self(
358
            $file,
359
            $alias,
360
            $basePath,
361
            $composerJson,
362
            $composerLock,
363
            $filesAggregate,
364
            $binaryFilesAggregate,
365
            $autodiscoverFiles || $forceFilesAutodiscovery,
366
            $dumpAutoload,
367
            $excludeComposerFiles,
368
            $compactors,
369
            $compressionAlgorithm,
370
            $fileMode,
371
            $mainScriptPath,
372
            $mainScriptContents,
373
            $fileMapper,
374
            $metadata,
375
            $tmpOutputPath,
376
            $outputPath,
377
            $privateKeyPassphrase,
378
            $privateKeyPath,
379
            $promptForPrivateKey,
380
            $replacements,
381
            $shebang,
382
            $signingAlgorithm,
383
            $stubBannerContents,
384
            $stubBannerPath,
385
            $stubPath,
386
            $isInterceptsFileFuncs,
387
            $isStubGenerated,
388
            $checkRequirements,
389
            $logger->getWarnings(),
390
            $logger->getRecommendations()
391
        );
392
    }
393
394
    /**
395
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
396
     *                                            path relative to it (the base path)
397
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
398
     *                                            string and the second element its decoded contents as an
399
     *                                            associative array.
400
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
401
     *                                            string and the second element its decoded contents as an
402
     *                                            associative array.
403
     * @param SplFileInfo[] $files                List of files
404
     * @param SplFileInfo[] $binaryFiles          List of binary files
405
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
406
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
407
     *                                            installed.json should be removed from the PHAR
408
     * @param Compactor[]   $compactors           List of file contents compactors
409
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
410
     * @param null|int      $fileMode             File mode in octal form
411
     * @param string        $mainScriptPath       The main script file path
412
     * @param string        $mainScriptContents   The processed content of the main script file
413
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
414
     * @param mixed         $metadata             The PHAR Metadata
415
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
416
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
417
     * @param null|string   $shebang              The shebang line
418
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
419
     * @param null|string   $stubBannerContents   The stub banner comment
420
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
421
     * @param null|string   $stubPath             The PHAR stub file path
422
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
423
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
424
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
425
     *                                            running
426
     * @param string[]      $warnings
427
     * @param string[]      $recommendations
428
     */
429
    private function __construct(
430
        ?string $file,
431
        string $alias,
432
        string $basePath,
433
        array $composerJson,
434
        array $composerLock,
435
        array $files,
436
        array $binaryFiles,
437
        bool $autodiscoveredFiles,
438
        bool $dumpAutoload,
439
        bool $excludeComposerFiles,
440
        array $compactors,
441
        ?int $compressionAlgorithm,
442
        ?int $fileMode,
443
        ?string $mainScriptPath,
444
        ?string $mainScriptContents,
445
        MapFile $fileMapper,
446
        $metadata,
447
        string $tmpOutputPath,
448
        string $outputPath,
449
        ?string $privateKeyPassphrase,
450
        ?string $privateKeyPath,
451
        bool $promptForPrivateKey,
452
        array $replacements,
453
        ?string $shebang,
454
        int $signingAlgorithm,
455
        ?string $stubBannerContents,
456
        ?string $stubBannerPath,
457
        ?string $stubPath,
458
        bool $isInterceptFileFuncs,
459
        bool $isStubGenerated,
460
        bool $checkRequirements,
461
        array $warnings,
462
        array $recommendations
463
    ) {
464
        Assertion::nullOrInArray(
465
            $compressionAlgorithm,
466
            get_phar_compression_algorithms(),
467
            sprintf(
468
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
469
                implode('", "', array_keys(get_phar_compression_algorithms()))
470
            )
471
        );
472
473
        if (null === $mainScriptPath) {
474
            Assertion::null($mainScriptContents);
475
        } else {
476
            Assertion::notNull($mainScriptContents);
477
        }
478
479
        $this->file = $file;
480
        $this->alias = $alias;
481
        $this->basePath = $basePath;
482
        $this->composerJson = $composerJson;
483
        $this->composerLock = $composerLock;
484
        $this->files = $files;
485
        $this->binaryFiles = $binaryFiles;
486
        $this->autodiscoveredFiles = $autodiscoveredFiles;
487
        $this->dumpAutoload = $dumpAutoload;
488
        $this->excludeComposerFiles = $excludeComposerFiles;
489
        $this->compactors = $compactors;
490
        $this->compressionAlgorithm = $compressionAlgorithm;
491
        $this->fileMode = $fileMode;
492
        $this->mainScriptPath = $mainScriptPath;
493
        $this->mainScriptContents = $mainScriptContents;
494
        $this->fileMapper = $fileMapper;
495
        $this->metadata = $metadata;
496
        $this->tmpOutputPath = $tmpOutputPath;
497
        $this->outputPath = $outputPath;
498
        $this->privateKeyPassphrase = $privateKeyPassphrase;
499
        $this->privateKeyPath = $privateKeyPath;
500
        $this->promptForPrivateKey = $promptForPrivateKey;
501
        $this->processedReplacements = $replacements;
502
        $this->shebang = $shebang;
503
        $this->signingAlgorithm = $signingAlgorithm;
504
        $this->stubBannerContents = $stubBannerContents;
505
        $this->stubBannerPath = $stubBannerPath;
506
        $this->stubPath = $stubPath;
507
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
508
        $this->isStubGenerated = $isStubGenerated;
509
        $this->checkRequirements = $checkRequirements;
510
        $this->warnings = $warnings;
511
        $this->recommendations = $recommendations;
512
    }
513
514
    public function getConfigurationFile(): ?string
515
    {
516
        return $this->file;
517
    }
518
519
    public function getAlias(): string
520
    {
521
        return $this->alias;
522
    }
523
524
    public function getBasePath(): string
525
    {
526
        return $this->basePath;
527
    }
528
529
    public function getComposerJson(): ?string
530
    {
531
        return $this->composerJson[0];
532
    }
533
534
    public function getDecodedComposerJsonContents(): ?array
535
    {
536
        return $this->composerJson[1];
537
    }
538
539
    public function getComposerLock(): ?string
540
    {
541
        return $this->composerLock[0];
542
    }
543
544
    public function getDecodedComposerLockContents(): ?array
545
    {
546
        return $this->composerLock[1];
547
    }
548
549
    /**
550
     * @return SplFileInfo[]
551
     */
552
    public function getFiles(): array
553
    {
554
        return $this->files;
555
    }
556
557
    /**
558
     * @return SplFileInfo[]
559
     */
560
    public function getBinaryFiles(): array
561
    {
562
        return $this->binaryFiles;
563
    }
564
565
    public function hasAutodiscoveredFiles(): bool
566
    {
567
        return $this->autodiscoveredFiles;
568
    }
569
570
    public function dumpAutoload(): bool
571
    {
572
        return $this->dumpAutoload;
573
    }
574
575
    public function excludeComposerFiles(): bool
576
    {
577
        return $this->excludeComposerFiles;
578
    }
579
580
    /**
581
     * @return Compactor[] the list of compactors
582
     */
583
    public function getCompactors(): array
584
    {
585
        return $this->compactors;
586
    }
587
588
    public function getCompressionAlgorithm(): ?int
589
    {
590
        return $this->compressionAlgorithm;
591
    }
592
593
    public function getFileMode(): ?int
594
    {
595
        return $this->fileMode;
596
    }
597
598
    public function hasMainScript(): bool
599
    {
600
        return null !== $this->mainScriptPath;
601
    }
602
603
    public function getMainScriptPath(): string
604
    {
605
        Assertion::notNull(
606
            $this->mainScriptPath,
607
            'Cannot retrieve the main script path: no main script configured.'
608
        );
609
610
        return $this->mainScriptPath;
611
    }
612
613
    public function getMainScriptContents(): string
614
    {
615
        Assertion::notNull(
616
            $this->mainScriptPath,
617
            'Cannot retrieve the main script contents: no main script configured.'
618
        );
619
620
        return $this->mainScriptContents;
621
    }
622
623
    public function checkRequirements(): bool
624
    {
625
        return $this->checkRequirements;
626
    }
627
628
    public function getTmpOutputPath(): string
629
    {
630
        return $this->tmpOutputPath;
631
    }
632
633
    public function getOutputPath(): string
634
    {
635
        return $this->outputPath;
636
    }
637
638
    public function getFileMapper(): MapFile
639
    {
640
        return $this->fileMapper;
641
    }
642
643
    /**
644
     * @return mixed
645
     */
646
    public function getMetadata()
647
    {
648
        return $this->metadata;
649
    }
650
651
    public function getPrivateKeyPassphrase(): ?string
652
    {
653
        return $this->privateKeyPassphrase;
654
    }
655
656
    public function getPrivateKeyPath(): ?string
657
    {
658
        return $this->privateKeyPath;
659
    }
660
661
    /**
662
     * @deprecated Use promptForPrivateKey() instead
663
     */
664
    public function isPrivateKeyPrompt(): bool
665
    {
666
        return $this->promptForPrivateKey;
667
    }
668
669
    public function promptForPrivateKey(): bool
670
    {
671
        return $this->promptForPrivateKey;
672
    }
673
674
    /**
675
     * @return scalar[]
676
     */
677
    public function getReplacements(): array
678
    {
679
        return $this->processedReplacements;
680
    }
681
682
    public function getShebang(): ?string
683
    {
684
        return $this->shebang;
685
    }
686
687
    public function getSigningAlgorithm(): int
688
    {
689
        return $this->signingAlgorithm;
690
    }
691
692
    public function getStubBannerContents(): ?string
693
    {
694
        return $this->stubBannerContents;
695
    }
696
697
    public function getStubBannerPath(): ?string
698
    {
699
        return $this->stubBannerPath;
700
    }
701
702
    public function getStubPath(): ?string
703
    {
704
        return $this->stubPath;
705
    }
706
707
    public function isInterceptFileFuncs(): bool
708
    {
709
        return $this->isInterceptFileFuncs;
710
    }
711
712
    public function isStubGenerated(): bool
713
    {
714
        return $this->isStubGenerated;
715
    }
716
717
    /**
718
     * @return string[]
719
     */
720
    public function getWarnings(): array
721
    {
722
        return $this->warnings;
723
    }
724
725
    /**
726
     * @return string[]
727
     */
728
    public function getRecommendations(): array
729
    {
730
        return $this->recommendations;
731
    }
732
733
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
734
    {
735
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
736
737
        if (false === isset($raw->{self::ALIAS_KEY})) {
738
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
739
        }
740
741
        $alias = trim($raw->{self::ALIAS_KEY});
742
743
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
744
745
        if ($userStubUsed) {
746
            $logger->addWarning(
747
                sprintf(
748
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
749
                    self::ALIAS_KEY
750
                )
751
            );
752
        }
753
754
        return $alias;
755
    }
756
757
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
758
    {
759
        if (null === $file) {
760
            return getcwd();
761
        }
762
763
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
764
            return realpath(dirname($file));
765
        }
766
767
        $basePath = trim($raw->{self::BASE_PATH_KEY});
768
769
        Assertion::directory(
770
            $basePath,
771
            'The base path "%s" is not a directory or does not exist.'
772
        );
773
774
        $basePath = realpath($basePath);
775
        $defaultPath = realpath(dirname($file));
776
777
        if ($basePath === $defaultPath) {
778
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
779
        }
780
781
        return $basePath;
782
    }
783
784
    /**
785
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
786
     */
787
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
788
    {
789
        if (null === $file) {
790
            return true;
791
        }
792
793
        $associativeRaw = (array) $raw;
794
795
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
796
    }
797
798
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
799
    {
800
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
801
802
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
803
    }
804
805
    private static function retrieveBlacklistFilter(
806
        stdClass $raw,
807
        string $basePath,
808
        ConfigurationLogger $logger,
809
        ?string ...$excludedPaths
810
    ): array {
811
        $blacklist = array_flip(
812
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths)
813
        );
814
815
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
816
            if ($file->isLink()) {
817
                return false;
818
            }
819
820
            if (false === $file->getRealPath()) {
821
                return false;
822
            }
823
824
            if (array_key_exists($file->getRealPath(), $blacklist)) {
825
                return false;
826
            }
827
828
            return null;
829
        };
830
831
        return [array_keys($blacklist), $blacklistFilter];
832
    }
833
834
    /**
835
     * @param null[]|string[] $excludedPaths
836
     *
837
     * @return string[]
838
     */
839
    private static function retrieveBlacklist(
840
        stdClass $raw,
841
        string $basePath,
842
        ConfigurationLogger $logger,
843
        ?string ...$excludedPaths
844
    ): array {
845
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
846
847
        $normalizedBlacklist = array_map(
848
            static function (string $excludedPath) use ($basePath): string {
849
                return self::normalizePath($excludedPath, $basePath);
850
            },
851
            array_filter($excludedPaths)
852
        );
853
854
        /** @var string[] $blacklist */
855
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
856
857
        foreach ($blacklist as $file) {
858
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
859
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
860
        }
861
862
        return array_unique($normalizedBlacklist);
863
    }
864
865
    /**
866
     * @param string[] $excludedPaths
867
     * @param string[] $devPackages
868
     *
869
     * @return SplFileInfo[]
870
     */
871
    private static function collectFiles(
872
        stdClass $raw,
873
        string $basePath,
874
        ?string $mainScriptPath,
875
        Closure $blacklistFilter,
876
        array $excludedPaths,
877
        array $devPackages,
878
        array $composerFiles,
879
        array $composerJson,
880
        bool $autodiscoverFiles,
881
        bool $forceFilesAutodiscovery,
882
        ConfigurationLogger $logger
883
    ): array {
884
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $mainScriptPath, $logger)];
885
886
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
887
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
888
                $basePath,
889
                $composerJson[1],
890
                $devPackages,
891
                array_filter(
892
                    array_column($composerFiles, 0)
893
                ),
894
                $excludedPaths
895
            );
896
897
            $files[] = $filesToAppend;
898
899
            $files[] = self::retrieveAllFiles(
900
                $basePath,
901
                $directories,
902
                $mainScriptPath,
903
                $blacklistFilter,
904
                $excludedPaths,
905
                $devPackages
906
            );
907
        }
908
909
        if (false === $autodiscoverFiles) {
910
            $files[] = self::retrieveDirectories(
911
                $raw,
912
                self::DIRECTORIES_KEY,
913
                $basePath,
914
                $blacklistFilter,
915
                $excludedPaths,
916
                $logger
917
            );
918
919
            $filesFromFinders = self::retrieveFilesFromFinders(
920
                $raw,
921
                self::FINDER_KEY,
922
                $basePath,
923
                $blacklistFilter,
924
                $devPackages,
925
                $logger
926
            );
927
928
            foreach ($filesFromFinders as $filesFromFinder) {
929
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
930
                $files[] = $filesFromFinder;
931
            }
932
        }
933
934
        return self::retrieveFilesAggregate(...$files);
935
    }
936
937
    /**
938
     * @param string[] $excludedPaths
939
     * @param string[] $devPackages
940
     *
941
     * @return SplFileInfo[]
942
     */
943
    private static function collectBinaryFiles(
944
        stdClass $raw,
945
        string $basePath,
946
        ?string $mainScriptPath,
947
        Closure $blacklistFilter,
948
        array $excludedPaths,
949
        array $devPackages,
950
        ConfigurationLogger $logger
951
    ): array {
952
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, [], $mainScriptPath, $logger);
953
954
        $binaryDirectories = self::retrieveDirectories(
955
            $raw,
956
            self::DIRECTORIES_BIN_KEY,
957
            $basePath,
958
            $blacklistFilter,
959
            $excludedPaths,
960
            $logger
961
        );
962
963
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
964
            $raw,
965
            self::FINDER_BIN_KEY,
966
            $basePath,
967
            $blacklistFilter,
968
            $devPackages,
969
            $logger
970
        );
971
972
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
973
    }
974
975
    /**
976
     * @return SplFileInfo[]
977
     */
978
    private static function retrieveFiles(
979
        stdClass $raw,
980
        string $key,
981
        string $basePath,
982
        array $composerFiles,
983
        ?string $mainScriptPath,
984
        ConfigurationLogger $logger
985
    ): array {
986
        self::checkIfDefaultValue($logger, $raw, $key, []);
987
988
        $files = [];
989
990
        if (isset($composerFiles[0][0])) {
991
            $files[] = $composerFiles[0][0];
992
        }
993
994
        if (isset($composerFiles[1][1])) {
995
            $files[] = $composerFiles[1][0];
996
        }
997
998
        if (false === isset($raw->{$key})) {
999
            return $files;
1000
        }
1001
1002
        if ([] === (array) $raw->{$key}) {
1003
            return $files;
1004
        }
1005
1006
        $files = array_merge((array) $raw->{$key}, $files);
1007
1008
        Assertion::allString($files);
1009
1010
        $normalizePath = static function (string $file) use ($basePath, $key, $mainScriptPath): ?SplFileInfo {
1011
            $file = self::normalizePath($file, $basePath);
1012
1013
            Assertion::false(
1014
                is_link($file),
1015
                sprintf(
1016
                    'Cannot add the link "%s": links are not supported.',
1017
                    $file
1018
                )
1019
            );
1020
1021
            Assertion::file(
1022
                $file,
1023
                sprintf(
1024
                    '"%s" must contain a list of existing files. Could not find "%%s".',
1025
                    $key
1026
                )
1027
            );
1028
1029
            return $mainScriptPath === $file ? null : new SplFileInfo($file);
1030
        };
1031
1032
        return array_filter(array_map($normalizePath, $files));
1033
    }
1034
1035
    /**
1036
     * @param string   $key           Config property name
1037
     * @param string[] $excludedPaths
1038
     *
1039
     * @return iterable|SplFileInfo[]
1040
     */
1041
    private static function retrieveDirectories(
1042
        stdClass $raw,
1043
        string $key,
1044
        string $basePath,
1045
        Closure $blacklistFilter,
1046
        array $excludedPaths,
1047
        ConfigurationLogger $logger
1048
    ): iterable {
1049
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
1050
1051
        if ([] !== $directories) {
1052
            $finder = Finder::create()
1053
                ->files()
1054
                ->filter($blacklistFilter)
1055
                ->ignoreVCS(true)
1056
                ->in($directories)
1057
            ;
1058
1059
            foreach ($excludedPaths as $excludedPath) {
1060
                $finder->notPath($excludedPath);
1061
            }
1062
1063
            return $finder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type SplFileInfo[]|iterable.
Loading history...
1064
        }
1065
1066
        return [];
1067
    }
1068
1069
    /**
1070
     * @param string[] $devPackages
1071
     *
1072
     * @return iterable[]|SplFileInfo[][]
1073
     */
1074
    private static function retrieveFilesFromFinders(
1075
        stdClass $raw,
1076
        string $key,
1077
        string $basePath,
1078
        Closure $blacklistFilter,
1079
        array $devPackages,
1080
        ConfigurationLogger $logger
1081
    ): array {
1082
        self::checkIfDefaultValue($logger, $raw, $key, []);
1083
1084
        if (false === isset($raw->{$key})) {
1085
            return [];
1086
        }
1087
1088
        $finder = $raw->{$key};
1089
1090
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1091
    }
1092
1093
    /**
1094
     * @param iterable[]|SplFileInfo[][] $fileIterators
1095
     *
1096
     * @return SplFileInfo[]
1097
     */
1098
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1099
    {
1100
        $files = [];
1101
1102
        foreach ($fileIterators as $fileIterator) {
1103
            foreach ($fileIterator as $file) {
1104
                $files[(string) $file] = $file;
1105
            }
1106
        }
1107
1108
        return array_values($files);
1109
    }
1110
1111
    /**
1112
     * @param string[] $devPackages
1113
     *
1114
     * @return Finder[]|SplFileInfo[][]
1115
     */
1116
    private static function processFinders(
1117
        array $findersConfig,
1118
        string $basePath,
1119
        Closure $blacklistFilter,
1120
        array $devPackages
1121
    ): array {
1122
        $processFinderConfig = static function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1123
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1124
        };
1125
1126
        return array_map($processFinderConfig, $findersConfig);
1127
    }
1128
1129
    /**
1130
     * @param string[] $devPackages
1131
     *
1132
     * @return Finder|SplFileInfo[]
1133
     */
1134
    private static function processFinder(
1135
        stdClass $config,
1136
        string $basePath,
1137
        Closure $blacklistFilter,
1138
        array $devPackages
1139
    ): Finder {
1140
        $finder = Finder::create()
1141
            ->files()
1142
            ->filter($blacklistFilter)
1143
            ->filter(
1144
                static function (SplFileInfo $fileInfo) use ($devPackages): bool {
1145
                    foreach ($devPackages as $devPackage) {
1146
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1147
                            // File belongs to the dev package
1148
                            return false;
1149
                        }
1150
                    }
1151
1152
                    return true;
1153
                }
1154
            )
1155
            ->ignoreVCS(true)
1156
        ;
1157
1158
        $normalizedConfig = (static function (array $config, Finder $finder): array {
1159
            $normalizedConfig = [];
1160
1161
            foreach ($config as $method => $arguments) {
1162
                $method = trim($method);
1163
                $arguments = (array) $arguments;
1164
1165
                Assertion::methodExists(
1166
                    $method,
1167
                    $finder,
1168
                    'The method "Finder::%s" does not exist.'
1169
                );
1170
1171
                $normalizedConfig[$method] = $arguments;
1172
            }
1173
1174
            krsort($normalizedConfig);
1175
1176
            return $normalizedConfig;
1177
        })((array) $config, $finder);
1178
1179
        $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string {
1180
            $directory = self::normalizePath($directory, $basePath);
1181
1182
            Assertion::false(
1183
                is_link($directory),
1184
                sprintf(
1185
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1186
                    $directory
1187
                )
1188
            );
1189
1190
            Assertion::directory($directory);
1191
1192
            return $directory;
1193
        };
1194
1195
        $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1196
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
0 ignored issues
show
Bug introduced by
It seems like $fileOrDirectory can also be of type null; however, parameter $file of KevinGH\Box\Configuration::normalizePath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1196
            $fileOrDirectory = self::normalizePath(/** @scrutinizer ignore-type */ $fileOrDirectory, $basePath);
Loading history...
1197
1198
            Assertion::false(
1199
                is_link($fileOrDirectory),
1200
                sprintf(
1201
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1202
                    $fileOrDirectory
1203
                )
1204
            );
1205
1206
            Assertion::true(
1207
                file_exists($fileOrDirectory),
1208
                sprintf(
1209
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1210
                    $fileOrDirectory
1211
                )
1212
            );
1213
1214
            if (false === is_file($fileOrDirectory)) {
1215
                Assertion::directory($fileOrDirectory);
1216
            } else {
1217
                Assertion::file($fileOrDirectory);
1218
            }
1219
1220
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1221
                $fileOrDirectory = null;
1222
            }
1223
        };
1224
1225
        foreach ($normalizedConfig as $method => $arguments) {
1226
            if ('in' === $method) {
1227
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1228
            }
1229
1230
            if ('exclude' === $method) {
1231
                $arguments = array_unique(array_map('trim', $arguments));
1232
            }
1233
1234
            if ('append' === $method) {
1235
                array_walk($arguments, $normalizeFileOrDirectory);
1236
1237
                $arguments = [array_filter($arguments)];
1238
            }
1239
1240
            foreach ($arguments as $argument) {
1241
                $finder->$method($argument);
1242
            }
1243
        }
1244
1245
        return $finder;
1246
    }
1247
1248
    /**
1249
     * @param string[] $devPackages
1250
     * @param string[] $filesToAppend
1251
     *
1252
     * @return string[][]
1253
     */
1254
    private static function retrieveAllDirectoriesToInclude(
1255
        string $basePath,
1256
        ?array $decodedJsonContents,
1257
        array $devPackages,
1258
        array $filesToAppend,
1259
        array $excludedPaths
1260
    ): array {
1261
        $toString = static function ($file): string {
1262
            // @param string|SplFileInfo $file
1263
            return (string) $file;
1264
        };
1265
1266
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1267
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1268
        } else {
1269
            $vendorDir = self::normalizePath('vendor', $basePath);
1270
        }
1271
1272
        if (file_exists($vendorDir)) {
1273
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1274
            // dependencies are included in the `composer.json`
1275
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1276
1277
            if (file_exists($installedJsonFiles)) {
1278
                $filesToAppend[] = $installedJsonFiles;
1279
            }
1280
1281
            $vendorPackages = toArray(values(map(
1282
                $toString,
1283
                Finder::create()
1284
                    ->in($vendorDir)
1285
                    ->directories()
1286
                    ->depth(1)
1287
                    ->ignoreUnreadableDirs()
1288
                    ->filter(
1289
                        static function (SplFileInfo $fileInfo): ?bool {
1290
                            if ($fileInfo->isLink()) {
1291
                                return false;
1292
                            }
1293
1294
                            return null;
1295
                        }
1296
                    )
1297
            )));
1298
1299
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1300
1301
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1302
                $files = toArray(values(map(
1303
                    $toString,
1304
                    Finder::create()
1305
                        ->in($basePath)
1306
                        ->files()
1307
                        ->depth(0)
1308
                )));
1309
1310
                $directories = toArray(values(map(
1311
                    $toString,
1312
                    Finder::create()
1313
                        ->in($basePath)
1314
                        ->notPath('vendor')
1315
                        ->directories()
1316
                        ->depth(0)
1317
                )));
1318
1319
                return [
1320
                    array_merge($files, $filesToAppend),
1321
                    array_merge($directories, $vendorPackages),
1322
                ];
1323
            }
1324
1325
            $paths = $vendorPackages;
1326
        } else {
1327
            $paths = [];
1328
        }
1329
1330
        $autoload = $decodedJsonContents['autoload'] ?? [];
1331
1332
        if (array_key_exists('psr-4', $autoload)) {
1333
            foreach ($autoload['psr-4'] as $path) {
1334
                /** @var string|string[] $path */
1335
                $composerPaths = (array) $path;
1336
1337
                foreach ($composerPaths as $composerPath) {
1338
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1339
                }
1340
            }
1341
        }
1342
1343
        if (array_key_exists('psr-0', $autoload)) {
1344
            foreach ($autoload['psr-0'] as $path) {
1345
                /** @var string|string[] $path */
1346
                $composerPaths = (array) $path;
1347
1348
                foreach ($composerPaths as $composerPath) {
1349
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1350
                }
1351
            }
1352
        }
1353
1354
        if (array_key_exists('classmap', $autoload)) {
1355
            foreach ($autoload['classmap'] as $path) {
1356
                // @var string $path
1357
                $paths[] = $path;
1358
            }
1359
        }
1360
1361
        $normalizePath = static function (string $path) use ($basePath): string {
1362
            return is_absolute_path($path)
1363
                ? canonicalize($path)
1364
                : self::normalizePath(trim($path, '/ '), $basePath)
1365
            ;
1366
        };
1367
1368
        if (array_key_exists('files', $autoload)) {
1369
            foreach ($autoload['files'] as $path) {
1370
                // @var string $path
1371
                $path = $normalizePath($path);
1372
1373
                Assertion::file($path);
1374
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1375
1376
                $filesToAppend[] = $path;
1377
            }
1378
        }
1379
1380
        $files = $filesToAppend;
1381
        $directories = [];
1382
1383
        foreach ($paths as $path) {
1384
            $path = $normalizePath($path);
1385
1386
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1387
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1388
1389
            if (is_file($path)) {
1390
                $files[] = $path;
1391
            } else {
1392
                $directories[] = $path;
1393
            }
1394
        }
1395
1396
        [$files, $directories] = [
1397
            array_unique($files),
1398
            array_unique($directories),
1399
        ];
1400
1401
        return [
1402
            array_diff($files, $excludedPaths),
1403
            array_diff($directories, $excludedPaths),
1404
        ];
1405
    }
1406
1407
    /**
1408
     * @param string[] $files
1409
     * @param string[] $directories
1410
     * @param string[] $excludedPaths
1411
     * @param string[] $devPackages
1412
     *
1413
     * @return SplFileInfo[]
1414
     */
1415
    private static function retrieveAllFiles(
1416
        string $basePath,
1417
        array $directories,
1418
        ?string $mainScriptPath,
1419
        Closure $blacklistFilter,
1420
        array $excludedPaths,
1421
        array $devPackages
1422
    ): iterable {
1423
        if ([] === $directories) {
1424
            return [];
1425
        }
1426
1427
        $relativeDevPackages = array_map(
1428
            static function (string $packagePath) use ($basePath): string {
1429
                return make_path_relative($packagePath, $basePath);
1430
            },
1431
            $devPackages
1432
        );
1433
1434
        $finder = Finder::create()
1435
            ->files()
1436
            ->filter($blacklistFilter)
1437
            ->exclude($relativeDevPackages)
1438
            ->ignoreVCS(true)
1439
            ->ignoreDotFiles(true)
1440
            // Remove build files
1441
            ->notName('composer.json')
1442
            ->notName('composer.lock')
1443
            ->notName('Makefile')
1444
            ->notName('Vagrantfile')
1445
            ->notName('phpstan*.neon*')
1446
            ->notName('infection*.json*')
1447
            ->notName('humbug*.json*')
1448
            ->notName('easy-coding-standard.neon*')
1449
            ->notName('phpbench.json*')
1450
            ->notName('phpcs.xml*')
1451
            ->notName('psalm.xml*')
1452
            ->notName('scoper.inc*')
1453
            ->notName('box*.json*')
1454
            ->notName('phpdoc*.xml*')
1455
            ->notName('codecov.yml*')
1456
            ->notName('Dockerfile')
1457
            ->exclude('build')
1458
            ->exclude('dist')
1459
            ->exclude('example')
1460
            ->exclude('examples')
1461
            // Remove documentation
1462
            ->notName('*.md')
1463
            ->notName('*.rst')
1464
            ->notName('/^readme(\..*+)?$/i')
1465
            ->notName('/^upgrade(\..*+)?$/i')
1466
            ->notName('/^contributing(\..*+)?$/i')
1467
            ->notName('/^changelog(\..*+)?$/i')
1468
            ->notName('/^authors?(\..*+)?$/i')
1469
            ->notName('/^conduct(\..*+)?$/i')
1470
            ->notName('/^todo(\..*+)?$/i')
1471
            ->exclude('doc')
1472
            ->exclude('docs')
1473
            ->exclude('documentation')
1474
            // Remove backup files
1475
            ->notName('*~')
1476
            ->notName('*.back')
1477
            ->notName('*.swp')
1478
            // Remove tests
1479
            ->notName('*Test.php')
1480
            ->exclude('test')
1481
            ->exclude('Test')
1482
            ->exclude('tests')
1483
            ->exclude('Tests')
1484
            ->notName('/phpunit.*\.xml(.dist)?/')
1485
            ->notName('/behat.*\.yml(.dist)?/')
1486
            ->exclude('spec')
1487
            ->exclude('specs')
1488
            ->exclude('features')
1489
            // Remove CI config
1490
            ->exclude('travis')
1491
            ->notName('travis.yml')
1492
            ->notName('appveyor.yml')
1493
            ->notName('build.xml*')
1494
        ;
1495
1496
        if (null !== $mainScriptPath) {
1497
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1498
        }
1499
1500
        $finder->in($directories);
1501
1502
        $excludedPaths = array_unique(
1503
            array_filter(
1504
                array_map(
1505
                    static function (string $path) use ($basePath): string {
1506
                        return make_path_relative($path, $basePath);
1507
                    },
1508
                    $excludedPaths
1509
                ),
1510
                static function (string $path): bool {
1511
                    return '..' !== substr($path, 0, 2);
1512
                }
1513
            )
1514
        );
1515
1516
        foreach ($excludedPaths as $excludedPath) {
1517
            $finder->notPath($excludedPath);
1518
        }
1519
1520
        return $finder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type SplFileInfo[].
Loading history...
1521
    }
1522
1523
    /**
1524
     * @param string $key Config property name
1525
     *
1526
     * @return string[]
1527
     */
1528
    private static function retrieveDirectoryPaths(
1529
        stdClass $raw,
1530
        string $key,
1531
        string $basePath,
1532
        ConfigurationLogger $logger
1533
    ): array {
1534
        self::checkIfDefaultValue($logger, $raw, $key, []);
1535
1536
        if (false === isset($raw->{$key})) {
1537
            return [];
1538
        }
1539
1540
        $directories = $raw->{$key};
1541
1542
        $normalizeDirectory = static function (string $directory) use ($basePath, $key): string {
1543
            $directory = self::normalizePath($directory, $basePath);
1544
1545
            Assertion::false(
1546
                is_link($directory),
1547
                sprintf(
1548
                    'Cannot add the link "%s": links are not supported.',
1549
                    $directory
1550
                )
1551
            );
1552
1553
            Assertion::directory(
1554
                $directory,
1555
                sprintf(
1556
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1557
                    $key
1558
                )
1559
            );
1560
1561
            return $directory;
1562
        };
1563
1564
        return array_map($normalizeDirectory, $directories);
1565
    }
1566
1567
    private static function normalizePath(string $file, string $basePath): string
1568
    {
1569
        return make_path_absolute(trim($file), $basePath);
1570
    }
1571
1572
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, ConfigurationLogger $logger): bool
1573
    {
1574
        self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true);
1575
1576
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1577
            return $composerJson;
1578
        }
1579
1580
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1581
1582
        if (false === $composerJson && $dumpAutoload) {
1583
            $logger->addWarning(
1584
                'The "dump-autoload" setting has been set but has been ignored because the composer.json file necessary'
1585
                .' for it could not be found'
1586
            );
1587
1588
            return false;
1589
        }
1590
1591
        return $composerJson && false !== $dumpAutoload;
1592
    }
1593
1594
    private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool
1595
    {
1596
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true);
1597
1598
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1599
    }
1600
1601
    /**
1602
     * @return Compactor[]
1603
     */
1604
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): array
1605
    {
1606
        self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []);
1607
1608
        $compactorClasses = array_unique((array) ($raw->{self::COMPACTORS_KEY} ?? []));
1609
1610
        $ignoredAnnotations = self::retrievePhpCompactorIgnoredAnnotations($raw, $compactorClasses, $logger);
1611
1612
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1613
            return [];
1614
        }
1615
1616
        $compators = array_map(
1617
            static function (string $class) use ($raw, $basePath, $logger, $ignoredAnnotations): Compactor {
1618
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1619
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1620
1621
                if (PhpCompactor::class === $class || LegacyPhp::class === $class) {
1622
                    return self::createPhpCompactor($ignoredAnnotations);
1623
                }
1624
1625
                if (PhpScoperCompactor::class === $class) {
1626
                    return self::createPhpScoperCompactor($raw, $basePath, $logger);
1627
                }
1628
1629
                return new $class();
1630
            },
1631
            $compactorClasses
1632
        );
1633
1634
        $scoperCompactor = false;
1635
1636
        foreach ($compators as $compactor) {
1637
            if ($compactor instanceof PhpScoperCompactor) {
1638
                $scoperCompactor = true;
1639
            }
1640
1641
            if ($compactor instanceof PhpCompactor) {
1642
                if (true === $scoperCompactor) {
1643
                    $logger->addRecommendation(
1644
                        sprintf(
1645
                            'The PHP compactor has been registered after the PhpScoper compactor. It is '
1646
                            .'recommended to register the PHP compactor before for a clearer code and faster processing.'
1647
                        )
1648
                    );
1649
                }
1650
1651
                break;
1652
            }
1653
        }
1654
1655
        return $compators;
1656
    }
1657
1658
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1659
    {
1660
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE');
1661
1662
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1663
            return null;
1664
        }
1665
1666
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1667
1668
        Assertion::inArray(
1669
            $raw->{self::COMPRESSION_KEY},
1670
            $knownAlgorithmNames,
1671
            sprintf(
1672
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1673
                implode('", "', $knownAlgorithmNames)
1674
            )
1675
        );
1676
1677
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1678
1679
        // Phar::NONE is not valid for compressFiles()
1680
        if (Phar::NONE === $value) {
1681
            return null;
1682
        }
1683
1684
        return $value;
1685
    }
1686
1687
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1688
    {
1689
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1690
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1691
        }
1692
1693
        $defaultChmod = intval(0755, 8);
1694
1695
        if (isset($raw->{self::CHMOD_KEY})) {
1696
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1697
1698
            if ($defaultChmod === $chmod) {
1699
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1700
            }
1701
1702
            return $chmod;
1703
        }
1704
1705
        return $defaultChmod;
1706
    }
1707
1708
    private static function retrieveMainScriptPath(
1709
        stdClass $raw,
1710
        string $basePath,
1711
        ?array $decodedJsonContents,
1712
        ConfigurationLogger $logger
1713
    ): ?string {
1714
        $firstBin = false;
1715
1716
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1717
            /** @var false|string $firstBin */
1718
            $firstBin = current((array) $decodedJsonContents['bin']);
1719
1720
            if (false !== $firstBin) {
1721
                $firstBin = self::normalizePath($firstBin, $basePath);
1722
            }
1723
        }
1724
1725
        if (isset($raw->{self::MAIN_KEY})) {
1726
            $main = $raw->{self::MAIN_KEY};
1727
1728
            if (is_string($main)) {
1729
                $main = self::normalizePath($main, $basePath);
1730
1731
                if ($main === $firstBin) {
1732
                    $logger->addRecommendation('The "main" setting can be omitted since is set to its default value');
1733
                }
1734
            }
1735
        } else {
1736
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1737
        }
1738
1739
        if (is_bool($main)) {
1740
            Assertion::false(
1741
                $main,
1742
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1743
            );
1744
1745
            return null;
1746
        }
1747
1748
        Assertion::file($main);
1749
1750
        return $main;
1751
    }
1752
1753
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1754
    {
1755
        if (null === $mainScriptPath) {
1756
            return null;
1757
        }
1758
1759
        $contents = file_contents($mainScriptPath);
1760
1761
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1762
        // PHAR entry point file.
1763
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1764
        // case the main script can be ignored or disabled.
1765
        return preg_replace('/^#!.*\s*/', '', $contents);
1766
    }
1767
1768
    /**
1769
     * @return string|null[][]
1770
     */
1771
    private static function retrieveComposerFiles(string $basePath): array
1772
    {
1773
        $retrieveFileAndContents = static function (string $file): array {
1774
            $json = new Json();
1775
1776
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1777
                return [null, null];
1778
            }
1779
1780
            try {
1781
                $contents = $json->decodeFile($file, true);
1782
            } catch (ParsingException $exception) {
1783
                throw new InvalidArgumentException(
1784
                    sprintf(
1785
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1786
                        $file,
1787
                        $exception->getMessage()
1788
                    ),
1789
                    0,
1790
                    $exception
1791
                );
1792
            }
1793
1794
            return [$file, $contents];
1795
        };
1796
1797
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1798
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1799
1800
        return [
1801
            [$composerJson, $composerJsonContents],
1802
            [$composerLock, $composerLockContents],
1803
        ];
1804
    }
1805
1806
    /**
1807
     * @return string[][]
1808
     */
1809
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
1810
    {
1811
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
1812
1813
        if (false === isset($raw->{self::MAP_KEY})) {
1814
            return [];
1815
        }
1816
1817
        $map = [];
1818
        $rawMap = (array) $raw->{self::MAP_KEY};
1819
1820
        foreach ($rawMap as $item) {
1821
            $processed = [];
1822
1823
            foreach ($item as $match => $replace) {
1824
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1825
            }
1826
1827
            if (isset($processed['_empty_'])) {
1828
                $processed[''] = $processed['_empty_'];
1829
1830
                unset($processed['_empty_']);
1831
            }
1832
1833
            $map[] = $processed;
1834
        }
1835
1836
        return $map;
1837
    }
1838
1839
    /**
1840
     * @return mixed
1841
     */
1842
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
1843
    {
1844
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
1845
1846
        if (false === isset($raw->{self::METADATA_KEY})) {
1847
            return null;
1848
        }
1849
1850
        $metadata = $raw->{self::METADATA_KEY};
1851
1852
        return is_object($metadata) ? (array) $metadata : $metadata;
1853
    }
1854
1855
    /**
1856
     * @return string[] The first element is the temporary output path and the second the final one
1857
     */
1858
    private static function retrieveOutputPath(
1859
        stdClass $raw,
1860
        string $basePath,
1861
        ?string $mainScriptPath,
1862
        ConfigurationLogger $logger
1863
    ): array {
1864
        $defaultPath = null;
1865
1866
        if (null !== $mainScriptPath
1867
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches)
1868
        ) {
1869
            $defaultPath = $matches['main'].'.phar';
1870
        }
1871
1872
        if (isset($raw->{self::OUTPUT_KEY})) {
1873
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
1874
1875
            if ($path === $defaultPath) {
1876
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
1877
            }
1878
        } elseif (null !== $defaultPath) {
1879
            $path = $defaultPath;
1880
        } else {
1881
            // Last resort, should not happen
1882
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
1883
        }
1884
1885
        $tmp = $real = $path;
1886
1887
        if ('.phar' !== substr($real, -5)) {
1888
            $tmp .= '.phar';
1889
        }
1890
1891
        return [$tmp, $real];
1892
    }
1893
1894
    private static function retrievePrivateKeyPath(
1895
        stdClass $raw,
1896
        string $basePath,
1897
        int $signingAlgorithm,
1898
        ConfigurationLogger $logger
1899
    ): ?string {
1900
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
1901
            if (null === $raw->{self::KEY_KEY}) {
1902
                $logger->addRecommendation(
1903
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
1904
                );
1905
            } else {
1906
                $logger->addWarning(
1907
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
1908
                );
1909
            }
1910
1911
            return null;
1912
        }
1913
1914
        if (!isset($raw->{self::KEY_KEY})) {
1915
            Assertion::true(
1916
                Phar::OPENSSL !== $signingAlgorithm,
1917
                'Expected to have a private key for OpenSSL signing but none have been provided.'
1918
            );
1919
1920
            return null;
1921
        }
1922
1923
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
1924
1925
        Assertion::file($path);
1926
1927
        return $path;
1928
    }
1929
1930
    private static function retrievePrivateKeyPassphrase(
1931
        stdClass $raw,
1932
        int $algorithm,
1933
        ConfigurationLogger $logger
1934
    ): ?string {
1935
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
1936
1937
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
1938
            return null;
1939
        }
1940
1941
        /** @var null|false|string $keyPass */
1942
        $keyPass = $raw->{self::KEY_PASS_KEY};
1943
1944
        if (Phar::OPENSSL !== $algorithm) {
1945
            if (false === $keyPass || null === $keyPass) {
1946
                $logger->addRecommendation(
1947
                    sprintf(
1948
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
1949
                        .'not "OPENSSL".',
1950
                        self::KEY_PASS_KEY
1951
                    )
1952
                );
1953
            } else {
1954
                $logger->addWarning(
1955
                    sprintf(
1956
                        'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
1957
                        self::KEY_PASS_KEY
1958
                    )
1959
                );
1960
            }
1961
1962
            return null;
1963
        }
1964
1965
        return is_string($keyPass) ? $keyPass : null;
1966
    }
1967
1968
    /**
1969
     * @return scalar[]
1970
     */
1971
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
1972
    {
1973
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
1974
1975
        if (null === $file) {
1976
            return [];
1977
        }
1978
1979
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
1980
1981
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
1982
            $replacements[$git] = self::retrievePrettyGitTag($file);
1983
        }
1984
1985
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
1986
            $replacements[$git] = self::retrieveGitHash($file);
1987
        }
1988
1989
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
1990
            $replacements[$git] = self::retrieveGitHash($file, true);
1991
        }
1992
1993
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
1994
            $replacements[$git] = self::retrieveGitTag($file);
1995
        }
1996
1997
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
1998
            $replacements[$git] = self::retrieveGitVersion($file);
1999
        }
2000
2001
        /**
2002
         * @var string
2003
         * @var bool   $valueSetByUser
2004
         */
2005
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
2006
2007
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
2008
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
2009
        } elseif ($valueSetByUser) {
2010
            $logger->addRecommendation(
2011
                sprintf(
2012
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
2013
                    self::DATETIME_FORMAT_KEY,
2014
                    self::DATETIME_KEY
2015
                )
2016
            );
2017
        }
2018
2019
        $sigil = self::retrieveReplacementSigil($raw, $logger);
2020
2021
        foreach ($replacements as $key => $value) {
2022
            unset($replacements[$key]);
2023
            $replacements[$sigil.$key.$sigil] = $value;
2024
        }
2025
2026
        return $replacements;
2027
    }
2028
2029
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2030
    {
2031
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
2032
    }
2033
2034
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2035
    {
2036
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
2037
    }
2038
2039
    /**
2040
     * @param bool $short Use the short version
2041
     *
2042
     * @return string the commit hash
2043
     */
2044
    private static function retrieveGitHash(string $file, bool $short = false): string
2045
    {
2046
        return self::runGitCommand(
2047
            sprintf(
2048
                'git log --pretty="%s" -n1 HEAD',
2049
                $short ? '%h' : '%H'
2050
            ),
2051
            $file
2052
        );
2053
    }
2054
2055
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2056
    {
2057
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
2058
    }
2059
2060
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2061
    {
2062
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
2063
    }
2064
2065
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
2066
    {
2067
        self::checkIfDefaultValue($logger, $raw, $key);
2068
2069
        return $raw->{$key} ?? null;
2070
    }
2071
2072
    private static function retrieveGitTag(string $file): string
2073
    {
2074
        return self::runGitCommand('git describe --tags HEAD', $file);
2075
    }
2076
2077
    private static function retrievePrettyGitTag(string $file): string
2078
    {
2079
        $version = self::retrieveGitTag($file);
2080
2081
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2082
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2083
        }
2084
2085
        return $version;
2086
    }
2087
2088
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2089
    {
2090
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2091
    }
2092
2093
    private static function retrieveGitVersion(string $file): ?string
2094
    {
2095
        try {
2096
            return self::retrieveGitTag($file);
2097
        } catch (RuntimeException $exception) {
2098
            try {
2099
                return self::retrieveGitHash($file, true);
2100
            } catch (RuntimeException $exception) {
2101
                throw new RuntimeException(
2102
                    sprintf(
2103
                        'The tag or commit hash could not be retrieved from "%s": %s',
2104
                        dirname($file),
2105
                        $exception->getMessage()
2106
                    ),
2107
                    0,
2108
                    $exception
2109
                );
2110
            }
2111
        }
2112
    }
2113
2114
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2115
    {
2116
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2117
    }
2118
2119
    private static function retrieveDatetimeNow(string $format): string
2120
    {
2121
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
2122
2123
        return $now->format($format);
2124
    }
2125
2126
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2127
    {
2128
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2129
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2130
2131
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2132
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2133
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2134
            @trigger_error(
2135
                'The "datetime_format" is deprecated, use "datetime-format" setting instead.',
2136
                E_USER_DEPRECATED
2137
            );
2138
            $logger->addWarning('The "datetime_format" is deprecated, use "datetime-format" setting instead.');
2139
2140
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2141
        } else {
2142
            $format = null;
2143
        }
2144
2145
        if (null !== $format) {
2146
            $formattedDate = (new DateTimeImmutable())->format($format);
2147
2148
            Assertion::false(
2149
                false === $formattedDate || $formattedDate === $format,
2150
                sprintf(
2151
                    'Expected the datetime format to be a valid format: "%s" is not',
2152
                    $format
2153
                )
2154
            );
2155
2156
            return [$format, true];
2157
        }
2158
2159
        return [self::DEFAULT_DATETIME_FORMAT, false];
2160
    }
2161
2162
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2163
    {
2164
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2165
    }
2166
2167
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2168
    {
2169
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2170
2171
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2172
            return self::DEFAULT_SHEBANG;
2173
        }
2174
2175
        $shebang = $raw->{self::SHEBANG_KEY};
2176
2177
        if (false === $shebang) {
2178
            if (false === $stubIsGenerated) {
2179
                $logger->addRecommendation(
2180
                    sprintf(
2181
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2182
                        .' being used',
2183
                        self::SHEBANG_KEY
2184
                    )
2185
                );
2186
            }
2187
2188
            return null;
2189
        }
2190
2191
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2192
2193
        $shebang = trim($shebang);
2194
2195
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2196
        Assertion::true(
2197
            '#!' === substr($shebang, 0, 2),
2198
            sprintf(
2199
                'The shebang line must start with "#!". Got "%s" instead',
2200
                $shebang
2201
            )
2202
        );
2203
2204
        if (false === $stubIsGenerated) {
2205
            $logger->addWarning(
2206
                sprintf(
2207
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2208
                    .' used',
2209
                    self::SHEBANG_KEY
2210
                )
2211
            );
2212
        }
2213
2214
        return $shebang;
2215
    }
2216
2217
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2218
    {
2219
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2220
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2221
        }
2222
2223
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2224
            return self::DEFAULT_SIGNING_ALGORITHM;
2225
        }
2226
2227
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2228
2229
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2230
2231
        Assertion::true(
2232
            defined('Phar::'.$algorithm),
2233
            sprintf(
2234
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2235
                $algorithm
2236
            )
2237
        );
2238
2239
        $algorithm = constant('Phar::'.$algorithm);
2240
2241
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2242
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2243
        }
2244
2245
        return $algorithm;
2246
    }
2247
2248
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2249
    {
2250
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::DEFAULT_BANNER);
2251
2252
        if (false === isset($raw->{self::BANNER_KEY})) {
2253
            return self::DEFAULT_BANNER;
2254
        }
2255
2256
        $banner = $raw->{self::BANNER_KEY};
2257
2258
        if (false === $banner) {
2259
            if (false === $stubIsGenerated) {
2260
                $logger->addRecommendation(
2261
                    sprintf(
2262
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2263
                        .'being used',
2264
                        self::BANNER_KEY
2265
                    )
2266
                );
2267
            }
2268
2269
            return null;
2270
        }
2271
2272
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2273
2274
        if (is_array($banner)) {
2275
            $banner = implode("\n", $banner);
2276
        }
2277
2278
        if (false === $stubIsGenerated) {
2279
            $logger->addWarning(
2280
                sprintf(
2281
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2282
                    self::BANNER_KEY
2283
                )
2284
            );
2285
        }
2286
2287
        return $banner;
2288
    }
2289
2290
    private static function retrieveStubBannerPath(
2291
        stdClass $raw,
2292
        string $basePath,
2293
        bool $stubIsGenerated,
2294
        ConfigurationLogger $logger
2295
    ): ?string {
2296
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2297
2298
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2299
            return null;
2300
        }
2301
2302
        $bannerFile = make_path_absolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2303
2304
        Assertion::file($bannerFile);
2305
2306
        if (false === $stubIsGenerated) {
2307
            $logger->addWarning(
2308
                sprintf(
2309
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2310
                    self::BANNER_FILE_KEY
2311
                )
2312
            );
2313
        }
2314
2315
        return $bannerFile;
2316
    }
2317
2318
    private static function normalizeStubBannerContents(?string $contents): ?string
2319
    {
2320
        if (null === $contents) {
2321
            return null;
2322
        }
2323
2324
        $banner = explode("\n", $contents);
2325
        $banner = array_map('trim', $banner);
2326
2327
        return implode("\n", $banner);
2328
    }
2329
2330
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2331
    {
2332
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2333
2334
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2335
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2336
2337
            Assertion::file($stubPath);
2338
2339
            return $stubPath;
2340
        }
2341
2342
        return null;
2343
    }
2344
2345
    private static function retrieveInterceptsFileFuncs(
2346
        stdClass $raw,
2347
        bool $stubIsGenerated,
2348
        ConfigurationLogger $logger
2349
    ): bool {
2350
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2351
2352
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2353
            return false;
2354
        }
2355
2356
        $intercept = $raw->{self::INTERCEPT_KEY};
2357
2358
        if ($intercept && false === $stubIsGenerated) {
2359
            $logger->addWarning(
2360
                sprintf(
2361
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2362
                    self::INTERCEPT_KEY
2363
                )
2364
            );
2365
        }
2366
2367
        return $intercept;
2368
    }
2369
2370
    private static function retrievePromptForPrivateKey(
2371
        stdClass $raw,
2372
        int $signingAlgorithm,
2373
        ConfigurationLogger $logger
2374
    ): bool {
2375
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2376
            if (Phar::OPENSSL !== $signingAlgorithm) {
2377
                $logger->addWarning(
2378
                    'A prompt for password for the private key has been requested but ignored since the signing '
2379
                    .'algorithm used is not "OPENSSL.'
2380
                );
2381
2382
                return false;
2383
            }
2384
2385
            return true;
2386
        }
2387
2388
        return false;
2389
    }
2390
2391
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2392
    {
2393
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2394
2395
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2396
    }
2397
2398
    private static function retrieveCheckRequirements(
2399
        stdClass $raw,
2400
        bool $hasComposerJson,
2401
        bool $hasComposerLock,
2402
        bool $pharStubUsed,
2403
        ConfigurationLogger $logger
2404
    ): bool {
2405
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2406
2407
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2408
            return $hasComposerJson || $hasComposerLock;
2409
        }
2410
2411
        /** @var bool $checkRequirements */
2412
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2413
2414
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2415
            $logger->addWarning(
2416
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2417
                .'be found.'
2418
            );
2419
2420
            return false;
2421
        }
2422
2423
        if ($checkRequirements && $pharStubUsed) {
2424
            $logger->addWarning(
2425
                sprintf(
2426
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2427
                    .'used.',
2428
                    self::CHECK_REQUIREMENTS_KEY
2429
                )
2430
            );
2431
        }
2432
2433
        return $checkRequirements;
2434
    }
2435
2436
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2437
    {
2438
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2439
2440
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2441
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2442
2443
            return file_exists($configFilePath)
2444
                ? PhpScoperConfiguration::load($configFilePath)
2445
                : PhpScoperConfiguration::load()
2446
             ;
2447
        }
2448
2449
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2450
2451
        Assertion::string($configFile);
2452
2453
        $configFilePath = make_path_absolute($configFile, $basePath);
2454
2455
        Assertion::file($configFilePath);
2456
        Assertion::readable($configFilePath);
2457
2458
        return PhpScoperConfiguration::load($configFilePath);
2459
    }
2460
2461
    /**
2462
     * Runs a Git command on the repository.
2463
     *
2464
     * @param string $command the command
2465
     *
2466
     * @return string the trimmed output from the command
2467
     */
2468
    private static function runGitCommand(string $command, string $file): string
2469
    {
2470
        $path = dirname($file);
2471
2472
        $process = new Process($command, $path);
2473
2474
        if (0 === $process->run()) {
2475
            return trim($process->getOutput());
2476
        }
2477
2478
        throw new RuntimeException(
2479
            sprintf(
2480
                'The tag or commit hash could not be retrieved from "%s": %s',
2481
                $path,
2482
                $process->getErrorOutput()
2483
            )
2484
        );
2485
    }
2486
2487
    /**
2488
     * @param string[] $compactorClasses
2489
     *
2490
     * @return string[]
2491
     */
2492
    private static function retrievePhpCompactorIgnoredAnnotations(
2493
        stdClass $raw,
2494
        array $compactorClasses,
2495
        ConfigurationLogger $logger
2496
    ): array {
2497
        $hasPhpCompactor = in_array(PhpCompactor::class, $compactorClasses, true) || in_array(LegacyPhp::class, $compactorClasses, true);
2498
2499
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, true);
2500
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, null);
2501
2502
        if (false === property_exists($raw, self::ANNOTATIONS_KEY)) {
2503
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2504
        }
2505
2506
        if (false === $hasPhpCompactor) {
2507
            $logger->addWarning(
2508
                sprintf(
2509
                    'The "%s" setting has been set but is ignored since no PHP compactor has been configured',
2510
                    self::ANNOTATIONS_KEY
2511
                )
2512
            );
2513
        }
2514
2515
        /** @var null|bool|stdClass $annotations */
2516
        $annotations = $raw->{self::ANNOTATIONS_KEY};
2517
2518
        if (true === $annotations || null === $annotations) {
2519
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2520
        }
2521
2522
        if (false === $annotations) {
0 ignored issues
show
introduced by
The condition false === $annotations is always true.
Loading history...
2523
            return [];
2524
        }
2525
2526
        if (false === property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)) {
2527
            $logger->addWarning(
2528
                sprintf(
2529
                    'The "%s" setting has been set but no "%s" setting has been found, hence "%s" is treated as'
2530
                    .' if it is set to `false`',
2531
                    self::ANNOTATIONS_KEY,
2532
                    self::IGNORED_ANNOTATIONS_KEY,
2533
                    self::ANNOTATIONS_KEY
2534
                )
2535
            );
2536
2537
            return [];
2538
        }
2539
2540
        $ignored = [];
2541
2542
        if (property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)
2543
            && in_array($ignored = $annotations->{self::IGNORED_ANNOTATIONS_KEY}, [null, []], true)
2544
        ) {
2545
            self::addRecommendationForDefaultValue($logger, self::ANNOTATIONS_KEY.'#'.self::IGNORED_ANNOTATIONS_KEY);
2546
2547
            return (array) $ignored;
2548
        }
2549
2550
        return $ignored;
2551
    }
2552
2553
    private static function createPhpCompactor(array $ignoredAnnotations): Compactor
2554
    {
2555
        $ignoredAnnotations = array_values(
2556
            array_filter(
2557
                array_map(
2558
                    static function (string $annotation): ?string {
2559
                        return strtolower(trim($annotation));
2560
                    },
2561
                    $ignoredAnnotations
2562
                )
2563
            )
2564
        );
2565
2566
        return new PhpCompactor(
2567
            new DocblockAnnotationParser(
2568
                new DocblockParser(),
2569
                new AnnotationDumper(),
2570
                $ignoredAnnotations
2571
            )
2572
        );
2573
    }
2574
2575
    private static function createPhpScoperCompactor(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactor
2576
    {
2577
        $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
2578
2579
        $phpScoper = (new class() extends ApplicationFactory {
2580
            public static function createScoper(): Scoper
2581
            {
2582
                return parent::createScoper();
2583
            }
2584
        })::createScoper();
2585
2586
        if ([] !== $phpScoperConfig->getWhitelistedFiles()) {
2587
            $whitelistedFiles = array_values(
2588
                array_unique(
2589
                    array_map(
2590
                        static function (string $path) use ($basePath): string {
2591
                            return make_path_relative($path, $basePath);
2592
                        },
2593
                        $phpScoperConfig->getWhitelistedFiles()
2594
                    )
2595
                )
2596
            );
2597
2598
            $phpScoper = new FileWhitelistScoper($phpScoper, ...$whitelistedFiles);
2599
        }
2600
2601
        $prefix = null === $phpScoperConfig->getPrefix()
2602
            ? unique_id('_HumbugBox')
2603
            : $phpScoperConfig->getPrefix()
2604
        ;
2605
2606
        return new PhpScoperCompactor(
2607
            new SimpleScoper(
2608
                $phpScoper,
2609
                $prefix,
2610
                $phpScoperConfig->getWhitelist(),
2611
                $phpScoperConfig->getPatchers()
2612
            )
2613
        );
2614
    }
2615
2616
    private static function checkIfDefaultValue(
2617
        ConfigurationLogger $logger,
2618
        stdClass $raw,
2619
        string $key,
2620
        $defaultValue = null
2621
    ): void {
2622
        if (false === property_exists($raw, $key)) {
2623
            return;
2624
        }
2625
2626
        $value = $raw->{$key};
2627
2628
        if (null === $value
2629
            || (false === is_object($defaultValue) && $defaultValue === $value)
2630
            || (is_object($defaultValue) && $defaultValue == $value)
2631
        ) {
2632
            $logger->addRecommendation(
2633
                sprintf(
2634
                    'The "%s" setting can be omitted since is set to its default value',
2635
                    $key
2636
                )
2637
            );
2638
        }
2639
    }
2640
2641
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2642
    {
2643
        $logger->addRecommendation(
2644
            sprintf(
2645
                'The "%s" setting can be omitted since is set to its default value',
2646
                $key
2647
            )
2648
        );
2649
    }
2650
}
2651