• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

violinist-dev / violinist-config / 14551652440

19 Apr 2025 06:11PM UTC coverage: 99.077% (-0.9%) from 100.0%
14551652440

Pull #41

github

eiriksm
phpstan fixes
Pull Request #41: Add methods for finding the correct extend responsible for setting things

61 of 64 new or added lines in 3 files covered. (95.31%)

1 existing line in 1 file now uncovered.

322 of 325 relevant lines covered (99.08%)

35.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.27
/src/Config.php
1
<?php
2

3
namespace Violinist\Config;
4

5
class Config
6
{
7
    private $config;
8
    private $configOptionsSet = [];
9
    private $matcherFactory;
10
    private $extendsChain = [];
11
    private $extendsStorage;
12

13
    const VIOLINIST_CONFIG_FILE = 'violinist-config.json';
14

15
    public function __construct()
16
    {
17
        $this->config = $this->getDefaultConfig();
208✔
18
        $this->extendsStorage = new ExtendsStorage();
208✔
19
    }
20

21
    public function getExtendsStorage()
22
    {
23
        return $this->extendsStorage;
6✔
24
    }
25

26
    public function getReadableChainForExtendName(string $name) : string
27
    {
28
        $chain = [$name];
1✔
29
        while (true) {
1✔
30
            $has_parent = false;
1✔
31
            foreach ($this->extendsChain as $parent => $child) {
1✔
32
                if ($child === $name) {
1✔
33
                    $has_parent = true;
1✔
34
                    if ($parent === '<root>') {
1✔
35
                        break 2;
1✔
36
                    }
37
                    $chain[] = $parent;
1✔
38
                    $name = $parent;
1✔
39
                    break;
1✔
40
                }
41
            }
42
            if ($has_parent) {
1✔
43
                continue;
1✔
44
            }
NEW
45
            throw new \RuntimeException('Could not find the parent of ' . $name . ' in extends chain');
×
46
        }
47
        $chain = array_reverse($chain);
1✔
48
        $chain = array_map(function ($item) {
1✔
49
            return sprintf('"%s"', $item);
1✔
50
        }, $chain);
1✔
51
        return implode(' -> ', $chain);
1✔
52
    }
53

54
    public function getExtendNameForKey(string $key)
55
    {
56
        // First find all the ones that actually are setting this in config.
57
        $extend_names = [];
1✔
58
        foreach ($this->extendsStorage->getExtendItems() as $extend_name => $items) {
1✔
59
            foreach ($items as $item) {
1✔
60
                if ($item->getKey() === $key) {
1✔
61
                    $extend_names[] = $extend_name;
1✔
62
                }
63
            }
64
        }
65
        // Now we need to consult our chain to see if we can find one on the
66
        // lowest level.
67
        $extend_name = '';
1✔
68
        foreach ($extend_names as $extend_name) {
1✔
69
            // Let's see if we can find a parent for it.
70
            foreach ($this->extendsChain as $parent => $child) {
1✔
71
                if ($child === $extend_name) {
1✔
72
                    // This means we have a parent for it. Let's see if we can
73
                    // find the parent in the list of extend names.
74
                    if (in_array($parent, $extend_names)) {
1✔
75
                        // Surely not it. Since the child sets it, it can not be
76
                        // the parent. So remove it from the extend names array.
77
                        $extend_names = array_diff($extend_names, [$parent]);
1✔
78
                    }
79
                }
80
            }
81
        }
82
        // At this point, hopefully we will have a single extend name left in
83
        // the array. If not, we will just return the first one.
84
        return reset($extend_names);
1✔
85
    }
86

87
    public function getDefaultConfig()
88
    {
89
        return (object) [
208✔
90
            'always_update_all' => 0,
208✔
91
            'always_allow_direct_dependencies' => 0,
208✔
92
            'ignore_platform_requirements' => 0,
208✔
93
            'allow_list' => [],
208✔
94
            'update_dev_dependencies' => 1,
208✔
95
            'check_only_direct_dependencies' => 1,
208✔
96
            'composer_outdated_flag' => 'minor',
208✔
97
            'bundled_packages' => (object) [],
208✔
98
            'blocklist' => [],
208✔
99
            'assignees' => [],
208✔
100
            'allow_updates_beyond_constraint' => 1,
208✔
101
            'one_pull_request_per_package' => 0,
208✔
102
            'timeframe_disallowed' => '',
208✔
103
            'timezone' => '+0000',
208✔
104
            'update_with_dependencies' => 1,
208✔
105
            'default_branch' => '',
208✔
106
            'default_branch_security' => '',
208✔
107
            'run_scripts' => 1,
208✔
108
            'security_updates_only' => 0,
208✔
109
            'number_of_concurrent_updates' => 0,
208✔
110
            'allow_security_updates_on_concurrent_limit' => 0,
208✔
111
            'branch_prefix' => '',
208✔
112
            'commit_message_convention' => '',
208✔
113
            'allow_update_indirect_with_direct' => 0,
208✔
114
            'automerge' => 0,
208✔
115
            'automerge_security' => 0,
208✔
116
            'automerge_method' => 'merge',
208✔
117
            'automerge_method_security' => 'merge',
208✔
118
            'labels' => [],
208✔
119
            'labels_security' => [],
208✔
120
        ];
208✔
121
    }
122

123
    public static function createFromComposerPath(string $path)
124
    {
125
        if (!file_exists($path)) {
9✔
126
            throw new \InvalidArgumentException('The path provided does not contain a composer.json file');
1✔
127
        }
128
        $composer_data = json_decode(file_get_contents($path));
8✔
129
        return self::createFromComposerDataInPath($composer_data, $path);
8✔
130
    }
131

132
    public static function createFromComposerDataInPath(\stdClass $data, string $path, string $initial_path = '', $parent = '')
133
    {
134
        // First we need the actual thing from the composer data.
135
        $instance = self::createFromComposerData($data);
8✔
136
        $extra_data = (object) [];
8✔
137
        if (!empty($data->extra->violinist)) {
8✔
138
            $extra_data = $data->extra->violinist;
8✔
139
        }
140
        $instance = self::handleExtendFromInstanceAndData($instance, $extra_data, $path, $initial_path, $parent);
8✔
141
        return $instance;
8✔
142
    }
143

144
    public static function handleExtendFromInstanceAndData(Config $instance, $data, $path, $initial_path = '', $parent = '') : Config
145
    {
146
        if (!$initial_path) {
8✔
147
            $initial_path = dirname($path);
8✔
148
        }
149
        // Now, is there a thing on the path in extends? Is there even
150
        // "extends"?
151
        if (empty($data->extends)) {
8✔
152
            return $instance;
7✔
153
        }
154
        $extends = $data->extends;
7✔
155
        // Remove the filename part of the path.
156
        $directory = dirname($path);
7✔
157
        $extends_path = $directory . '/' . $extends;
7✔
158
        $potential_places = [
7✔
159
            $extends_path,
7✔
160
            sprintf('%s/vendor/%s/%s', $directory, $extends, self::VIOLINIST_CONFIG_FILE),
7✔
161
            sprintf('%s/vendor/%s/composer.json', $directory, $extends),
7✔
162
            sprintf('%s/vendor/%s', $directory, $extends),
7✔
163
        ];
7✔
164
        if ($initial_path) {
7✔
165
            $potential_places[] = sprintf('%s/vendor/%s/%s', $initial_path, $extends, self::VIOLINIST_CONFIG_FILE);
7✔
166
            $potential_places[] = "$initial_path/vendor/$extends/composer.json";
7✔
167
        }
168
        foreach ($potential_places as $potential_place) {
7✔
169
            if (file_exists($potential_place) && !is_dir($potential_place)) {
7✔
170
                $extends_data = json_decode(file_get_contents($potential_place));
7✔
171
                if (!$extends_data) {
7✔
172
                    continue;
1✔
173
                }
174
                $extends_instance = self::createFromViolinistConfigInPath($extends_data, $potential_place, $initial_path, $extends);
6✔
175
                if (strpos($potential_place, 'composer.json') !== false) {
6✔
176
                    // This is a composer.json file. Let's create it from that.
177
                    $extends_instance = self::createFromComposerDataInPath($extends_data, $potential_place, $initial_path, $extends);
3✔
178
                }
179
                // Now merge the two.
180
                $extends_key = $parent;
6✔
181
                if (!$extends_key) {
6✔
182
                    $extends_key = '<root>';
6✔
183
                }
184
                // Copy over the extends chain to the new instance.
185
                $instance->extendsChain = $extends_instance->extendsChain;
6✔
186
                $instance->extendsChain[$extends_key] = $extends;
6✔
187
                $instance->mergeConfig($extends_instance, $extends, $parent);
6✔
188
                break;
6✔
189
            }
190
        }
191
        return $instance;
7✔
192
    }
193

194
    public function getConfig()
195
    {
196
        return $this->config;
6✔
197
    }
198

199
    public static function createFromComposerData($data)
200
    {
201
        $instance = new self();
208✔
202
        if (!empty($data->extra->violinist)) {
208✔
203
            $instance->setConfig($data->extra->violinist);
205✔
204
        }
205
        return $instance;
208✔
206
    }
207

208
    public static function createFromViolinistConfigInPath($data, $file_path, $initial_path = '', $parent = '')
209
    {
210
        $instance = self::createFromViolinistConfig($data);
6✔
211
        $instance = self::handleExtendFromInstanceAndData($instance, $data, $file_path, $initial_path, $parent);
6✔
212
        return $instance;
6✔
213
    }
214

215
    public static function createFromViolinistConfig($data)
216
    {
217
        $instance = new self();
6✔
218
        $instance->setConfig($data);
6✔
219
        return $instance;
6✔
220
    }
221

222
    public static function createFromViolinistConfigJsonString(string $data)
223
    {
224
        $json_data = json_decode($data, false, 512, JSON_THROW_ON_ERROR);
3✔
225
        return self::createFromViolinistConfig($json_data);
3✔
226
    }
227

228
    public function setConfig($config)
229
    {
230
        foreach ($this->getDefaultConfig() as $key => $value) {
205✔
231
            if (isset($config->{$key})) {
205✔
232
                $this->config->{$key} = $config->{$key};
162✔
233
                $this->configOptionsSet[$key] = true;
162✔
234
            }
235
        }
236
        // Also make sure to set the block list config from the deprecated part.
237
        // Plus alternative spelling from allow list.
238
        $renamed_and_aliased = [
205✔
239
            'blacklist' => 'blocklist',
205✔
240
            'block_list' => 'blocklist',
205✔
241
            'allowlist' => 'allow_list',
205✔
242
        ];
205✔
243
        foreach ($renamed_and_aliased as $not_real => $real) {
205✔
244
            if (isset($config->{$not_real})) {
205✔
245
                $this->config->{$real} = $config->{$not_real};
5✔
246
            }
247
        }
248
        if (!empty($config->rules)) {
205✔
249
            $this->config->rules = $config->rules;
2✔
250
        }
251
    }
252

253
    public function getComposerOutdatedFlag() : string
254
    {
255
        if (empty($this->config->composer_outdated_flag)) {
5✔
256
            return 'minor';
1✔
257
        }
258
        $allowed_values = [
4✔
259
            'major',
4✔
260
            'minor',
4✔
261
            'patch',
4✔
262
        ];
4✔
263
        if (!in_array($this->config->composer_outdated_flag, $allowed_values)) {
4✔
264
            return 'minor';
1✔
265
        }
266
        return $this->config->composer_outdated_flag;
3✔
267
    }
268

269
    public function getLabels() : array
270
    {
271
        if (!is_array($this->config->labels)) {
7✔
272
            return [];
3✔
273
        }
274
        return $this->config->labels;
4✔
275
    }
276

277
    public function getLabelsSecurity() : array
278
    {
279
        if (!is_array($this->config->labels_security)) {
7✔
280
            return [];
3✔
281
        }
282
        return $this->config->labels_security;
4✔
283
    }
284

285
    public function shouldAlwaysAllowDirect() : bool
286
    {
287
        return (bool) $this->config->always_allow_direct_dependencies;
6✔
288
    }
289

290
    public function hasConfigForKey($key)
291
    {
292
        return !empty($this->configOptionsSet[$key]);
18✔
293
    }
294

295
    public function shouldAutoMerge($is_security_update = false)
296
    {
297
        if (!$is_security_update) {
15✔
298
            // It's not a security update. Let's use the option found in the config.
299
            return (bool) $this->config->automerge;
6✔
300
        }
301
        if ($this->shouldAutoMergeSecurity()) {
9✔
302
            // Meaning we should automerge, no matter what the general automerge config says.
303
            return true;
2✔
304
        }
305
        // Fall back to using the actual option.
306
        return (bool) $this->config->automerge;
7✔
307
    }
308

309
    public function getAutomergeMethod($is_security_update = false) : string
310
    {
311
        if (!$is_security_update) {
26✔
312
            return $this->getAutoMergeMethodWithFallback('automerge_method');
13✔
313
        }
314
        // Otherwise, let's see if it's even set in config. Otherwise this
315
        // should be set to the value (or fallback value) of the general
316
        // automerge method.
317
        if ($this->hasConfigForKey('automerge_method_security')) {
13✔
318
            return $this->getAutoMergeMethodWithFallback('automerge_method_security');
6✔
319
        }
320
        return $this->getAutoMergeMethodWithFallback('automerge_method');
7✔
321
    }
322

323
    protected function getAutoMergeMethodWithFallback($automerge_property) : string
324
    {
325
        if (!in_array($this->config->{$automerge_property}, [
26✔
326
            'merge',
26✔
327
            'rebase',
26✔
328
            'squash',
26✔
329
        ])
26✔
330
        ) {
331
            return 'merge';
9✔
332
        }
333
        return $this->config->{$automerge_property};
17✔
334
    }
335

336
    public function shouldAutoMergeSecurity()
337
    {
338
        return (bool) $this->config->automerge_security;
9✔
339
    }
340

341
    public function shouldUpdateIndirectWithDirect()
342
    {
343
        return (bool) $this->config->allow_update_indirect_with_direct;
4✔
344
    }
345

346
    public function shouldAlwaysUpdateAll()
347
    {
348
        return (bool) $this->config->always_update_all;
6✔
349
    }
350

351
    public function getTimeZone()
352
    {
353
        if (!is_string($this->config->timezone)) {
5✔
354
            return '+0000';
2✔
355
        }
356
        if (empty($this->config->timezone)) {
3✔
357
            return '+0000';
1✔
358
        }
359
        return $this->config->timezone;
2✔
360
    }
361

362
    public function getTimeFrameDisallowed()
363
    {
364
        if (!is_string($this->config->timeframe_disallowed)) {
5✔
365
            return '';
1✔
366
        }
367
        if (empty($this->config->timeframe_disallowed)) {
4✔
368
            return '';
2✔
369
        }
370
        $frame = $this->config->timeframe_disallowed;
2✔
371
        $length = count(explode('-', $frame));
2✔
372
        if ($length !== 2) {
2✔
373
            throw new \InvalidArgumentException('The timeframe should consist of two 24 hour format times separated by a dash ("-")');
1✔
374
        }
375
        return $this->config->timeframe_disallowed;
1✔
376
    }
377

378
    public function shouldUpdateWithDependencies()
379
    {
380
        return (bool) $this->config->update_with_dependencies;
5✔
381
    }
382

383
    public function shouldAllowUpdatesBeyondConstraint()
384
    {
385
        return (bool) $this->config->allow_updates_beyond_constraint;
5✔
386
    }
387

388
    public function shouldRunScripts()
389
    {
390
        return (bool) $this->config->run_scripts;
8✔
391
    }
392

393
    public function getPackagesWithBundles()
394
    {
395
        $with_bundles = [];
7✔
396
        if (!is_object($this->config->bundled_packages)) {
7✔
397
            return [];
2✔
398
        }
399
        foreach ($this->config->bundled_packages as $package => $bundle) {
5✔
400
            if (!is_array($bundle)) {
3✔
401
                continue;
2✔
402
            }
403
            $with_bundles[] = $package;
1✔
404
        }
405
        return $with_bundles;
5✔
406
    }
407

408
    public function getBundledPackagesForPackage($package_name)
409
    {
410
        if (!is_object($this->config->bundled_packages)) {
6✔
411
            return [];
2✔
412
        }
413
        foreach ($this->config->bundled_packages as $package => $bundle) {
4✔
414
            if ($package === $package_name) {
3✔
415
                if (!is_array($bundle)) {
3✔
416
                    throw new \Exception('Found bundle for ' . $package . ' but the bundle was not an array');
2✔
417
                }
418
                return $bundle;
1✔
419
            }
420
        }
421
        return [];
1✔
422
    }
423

424
    public function getAssignees()
425
    {
426
        if (!is_array($this->config->assignees)) {
4✔
427
            return [];
1✔
428
        }
429

430
        return $this->config->assignees;
3✔
431
    }
432

433
    /**
434
     * Just an alias, since providers differ in their wording on this.
435
     */
436
    public function shouldUseOneMergeRequestPerPackage()
437
    {
438
        return $this->shouldUseOnePullRequestPerPackage();
5✔
439
    }
440

441
    public function shouldUseOnePullRequestPerPackage()
442
    {
443
        return (bool) $this->config->one_pull_request_per_package;
5✔
444
    }
445

446
    public function getBlockList()
447
    {
448
        if (!is_array($this->config->blocklist)) {
8✔
449
            return [];
2✔
450
        }
451

452
        return $this->config->blocklist;
6✔
453
    }
454

455
    public function getAllowList()
456
    {
457
        if (!is_array($this->config->allow_list)) {
4✔
458
            return [];
1✔
459
        }
460

461
        return $this->config->allow_list;
3✔
462
    }
463

464
    /**
465
     * @deprecated Use ::getBlockList instead.
466
     */
467
    public function getBlackList()
468
    {
469
        return $this->getBlockList();
8✔
470
    }
471

472
    public function shouldUpdateDevDependencies()
473
    {
474
        return (bool) $this->config->update_dev_dependencies;
11✔
475
    }
476

477
    public function getNumberOfAllowedPrs()
478
    {
479
        return (int) $this->config->number_of_concurrent_updates;
6✔
480
    }
481

482
    public function shouldAllowSecurityUpdatesOnConcurrentLimit()
483
    {
484
        return (bool) $this->config->allow_security_updates_on_concurrent_limit;
3✔
485
    }
486

487
    public function shouldOnlyUpdateSecurityUpdates()
488
    {
489
        return (bool) $this->config->security_updates_only;
8✔
490
    }
491

492
    public function getDefaultBranchSecurity()
493
    {
494
        return $this->getDefaultBranch(true);
7✔
495
    }
496

497
    public function getDefaultBranch($is_security = false)
498
    {
499
        if ($is_security && !empty($this->config->default_branch_security)) {
16✔
500
            return $this->config->default_branch_security;
2✔
501
        }
502
        if ($is_security && empty($this->config->default_branch_security)) {
14✔
503
            return $this->getDefaultBranch();
5✔
504
        }
505
        if (empty($this->config->default_branch)) {
14✔
506
            return false;
11✔
507
        }
508
        return $this->config->default_branch;
3✔
509
    }
510

511
    public function shouldCheckDirectOnly()
512
    {
513
        return (bool) $this->config->check_only_direct_dependencies;
8✔
514
    }
515

516
    public function getBranchPrefix()
517
    {
518
        if ($this->config->branch_prefix) {
6✔
519
            if (!is_string($this->config->branch_prefix)) {
4✔
520
                return '';
1✔
521
            }
522
            return (string) $this->config->branch_prefix;
3✔
523
        }
524
        return '';
2✔
525
    }
526

527
    public function shouldIgnorePlatformRequirements() : bool
528
    {
529
        return (bool) $this->config->ignore_platform_requirements;
6✔
530
    }
531

532
    public function getCommitMessageConvention()
533
    {
534
        if (!$this->config->commit_message_convention || !is_string($this->config->commit_message_convention)) {
4✔
535
            return '';
3✔
536
        }
537

538
        return $this->config->commit_message_convention;
1✔
539
    }
540

541
    public function getConfigForPackage(string $package_name) : self
542
    {
543
        $rules = $this->getRules();
3✔
544
        if (empty($rules)) {
3✔
545
            return $this;
1✔
546
        }
547
        $new_config = clone $this->config;
2✔
548
        foreach ($this->config->rules as $rule) {
2✔
549
            if (empty($rule->config)) {
2✔
550
                continue;
1✔
551
            }
552
            $matches = $this->getMatcherFactory()->hasMatches($rule, $package_name);
1✔
553
            if (!$matches) {
1✔
554
                continue;
1✔
555
            }
556
            // Then merge the config for this rule.
557
            $this->mergeConfigFromConfigObject($new_config, $rule->config);
1✔
558
        }
559
        return self::createFromViolinistConfig($new_config);
2✔
560
    }
561

562
    public function getRules() : array
563
    {
564
        if (!empty($this->config->rules)) {
3✔
565
            return $this->config->rules;
2✔
566
        }
567
        return [];
1✔
568
    }
569

570
    protected function mergeConfig(Config $other, $extends_name, $parent)
571
    {
572
        $keys_and_values_affected = $this->mergeConfigFromConfigObject($this->getConfig(), $other->getConfig());
6✔
573
        $this->extendsStorage->addExtendItems($other->getExtendsStorage()->getExtendItems());
6✔
574
        foreach ($keys_and_values_affected as $key => $value) {
6✔
575
            $this->configOptionsSet[$key] = true;
5✔
576
            $this->extendsStorage->addExtendItem(new ExtendsChainItem($extends_name, $key, $value));
5✔
577
        }
578
    }
579

580
    protected function mergeConfigFromConfigObject(\stdClass $config, \stdClass $other) : array
581
    {
582
        $affected = [];
6✔
583
        $default_config = $this->getDefaultConfig();
6✔
584
        foreach ($other as $key => $value) {
6✔
585
            // If the value corresponds to the default config, we don't need to
586
            // set it.
587
            if (isset($default_config->{$key}) && $default_config->{$key} === $value) {
6✔
588
                continue;
6✔
589
            }
590
            // This special case is because the default config is a stdclass,
591
            // and that will not pass the strict equal test. So let's just
592
            // loosen it up a bit for this specific case.
593
            if ($key === 'bundled_packages' && $default_config->{$key} == $value) {
6✔
594
                continue;
6✔
595
            }
596
            $config->{$key} = $value;
5✔
597
            $affected[$key] = $value;
5✔
598
        }
599
        return $affected;
6✔
600
    }
601

602
    public function getExtendsChain()
603
    {
NEW
UNCOV
604
        return $this->extendsChain;
×
605
    }
606

607
    public function getMatcherFactory() : MatcherFactory
608
    {
609
        if (!$this->matcherFactory) {
1✔
610
            $this->matcherFactory = new MatcherFactory();
1✔
611
        }
612
        return $this->matcherFactory;
1✔
613
    }
614
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc