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

violinist-dev / violinist-config / 23834295499

01 Apr 2026 05:57AM UTC coverage: 99.704% (+0.001%) from 99.703%
23834295499

Pull #46

github

eiriksm
Fix tests
Pull Request #46: Make it possible for a rule to override back to default

12 of 13 new or added lines in 1 file covered. (92.31%)

1 existing line in 1 file now uncovered.

337 of 338 relevant lines covered (99.7%)

36.36 hits per line

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

99.65
/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();
216✔
18
        $this->extendsStorage = new ExtendsStorage();
216✔
19
    }
20

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

26
    public function getReadableChainForExtendName(string $name) : string
27
    {
28
        $chain = [$name];
2✔
29
        while (true) {
2✔
30
            $has_parent = false;
2✔
31
            foreach ($this->extendsChain as $parent => $child) {
2✔
32
                if ($child === $name) {
2✔
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) {
2✔
43
                continue;
1✔
44
            }
45
            throw new \RuntimeException('Could not find the parent of ' . $name . ' in extends chain');
1✔
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) : string
55
    {
56
        // First find all the ones that actually are setting this in config.
57
        $extend_names = [];
2✔
58
        foreach ($this->extendsStorage->getExtendItems() as $extend_name => $items) {
2✔
59
            foreach ($items as $item) {
2✔
60
                if ($item->getKey() === $key) {
2✔
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 = '';
2✔
68
        foreach ($extend_names as $extend_name) {
2✔
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
        if (count($extend_names) === 0) {
2✔
83
            return '';
1✔
84
        }
85
        // At this point, hopefully we will have a single extend name left in
86
        // the array. If not, we will just return the first one.
87
        return reset($extend_names);
1✔
88
    }
89

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

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

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

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

197
    public function getConfig()
198
    {
199
        return $this->config;
8✔
200
    }
201

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

211
    public static function createFromViolinistConfigInPath($data, $file_path, $initial_path = '', $parent = '')
212
    {
213
        $instance = self::createFromViolinistConfig($data);
8✔
214
        $instance = self::handleExtendFromInstanceAndData($instance, $data, $file_path, $initial_path, $parent);
8✔
215
        return $instance;
8✔
216
    }
217

218
    public static function createFromViolinistConfig($data)
219
    {
220
        $instance = new self();
11✔
221
        $instance->setConfig($data);
11✔
222
        return $instance;
11✔
223
    }
224

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

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

256
    public function getComposerOutdatedFlag() : string
257
    {
258
        if (empty($this->config->composer_outdated_flag)) {
7✔
259
            return 'minor';
2✔
260
        }
261
        $allowed_values = [
5✔
262
            'major',
5✔
263
            'minor',
5✔
264
            'patch',
5✔
265
            'major-only',
5✔
266
        ];
5✔
267
        if (!in_array($this->config->composer_outdated_flag, $allowed_values, true)) {
5✔
268
            return 'minor';
1✔
269
        }
270
        return $this->config->composer_outdated_flag;
4✔
271
    }
272

273
    public function getLabels() : array
274
    {
275
        if (!is_array($this->config->labels)) {
7✔
276
            return [];
3✔
277
        }
278
        return $this->config->labels;
4✔
279
    }
280

281
    public function getLabelsSecurity() : array
282
    {
283
        if (!is_array($this->config->labels_security)) {
7✔
284
            return [];
3✔
285
        }
286
        return $this->config->labels_security;
4✔
287
    }
288

289
    public function shouldAlwaysAllowDirect() : bool
290
    {
291
        return (bool) $this->config->always_allow_direct_dependencies;
6✔
292
    }
293

294
    public function hasConfigForKey($key)
295
    {
296
        return !empty($this->configOptionsSet[$key]);
18✔
297
    }
298

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

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

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

340
    public function shouldAutoMergeSecurity()
341
    {
342
        return (bool) $this->config->automerge_security;
9✔
343
    }
344

345
    public function shouldUpdateIndirectWithDirect()
346
    {
347
        return (bool) $this->config->allow_update_indirect_with_direct;
4✔
348
    }
349

350
    public function shouldAlwaysUpdateAll()
351
    {
352
        return (bool) $this->config->always_update_all;
6✔
353
    }
354

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

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

382
    public function shouldUpdateWithDependencies()
383
    {
384
        return (bool) $this->config->update_with_dependencies;
5✔
385
    }
386

387
    public function shouldAllowUpdatesBeyondConstraint()
388
    {
389
        return (bool) $this->config->allow_updates_beyond_constraint;
5✔
390
    }
391

392
    public function shouldRunScripts()
393
    {
394
        return (bool) $this->config->run_scripts;
8✔
395
    }
396

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

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

428
    public function getAssignees()
429
    {
430
        if (!is_array($this->config->assignees)) {
4✔
431
            return [];
1✔
432
        }
433

434
        return $this->config->assignees;
3✔
435
    }
436

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

445
    public function shouldUseOnePullRequestPerPackage()
446
    {
447
        return (bool) $this->config->one_pull_request_per_package;
5✔
448
    }
449

450
    public function getBlockList()
451
    {
452
        if (!is_array($this->config->blocklist)) {
9✔
453
            return [];
2✔
454
        }
455

456
        return $this->config->blocklist;
7✔
457
    }
458

459
    public function getAllowList()
460
    {
461
        if (!is_array($this->config->allow_list)) {
4✔
462
            return [];
1✔
463
        }
464

465
        return $this->config->allow_list;
3✔
466
    }
467

468
    /**
469
     * @deprecated Use ::getBlockList instead.
470
     */
471
    public function getBlackList()
472
    {
473
        return $this->getBlockList();
8✔
474
    }
475

476
    public function shouldUpdateDevDependencies()
477
    {
478
        return (bool) $this->config->update_dev_dependencies;
13✔
479
    }
480

481
    public function getNumberOfAllowedPrs()
482
    {
483
        return (int) $this->config->number_of_concurrent_updates;
6✔
484
    }
485

486
    public function shouldAllowSecurityUpdatesOnConcurrentLimit()
487
    {
488
        return (bool) $this->config->allow_security_updates_on_concurrent_limit;
3✔
489
    }
490

491
    public function shouldOnlyUpdateSecurityUpdates()
492
    {
493
        return (bool) $this->config->security_updates_only;
9✔
494
    }
495

496
    public function getDefaultBranchSecurity()
497
    {
498
        return $this->getDefaultBranch(true);
7✔
499
    }
500

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

515
    public function shouldCheckDirectOnly()
516
    {
517
        return (bool) $this->config->check_only_direct_dependencies;
8✔
518
    }
519

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

531
    public function shouldIgnorePlatformRequirements() : bool
532
    {
533
        return (bool) $this->config->ignore_platform_requirements;
6✔
534
    }
535

536
    public function getCommitMessageConvention()
537
    {
538
        if (!$this->config->commit_message_convention || !is_string($this->config->commit_message_convention)) {
4✔
539
            return '';
3✔
540
        }
541

542
        return $this->config->commit_message_convention;
1✔
543
    }
544

545
    public function getConfigForRuleObject(\stdClass $rule_object)
546
    {
547
        // @todo: This is a bit duplication from below actually. Specifically
548
        // the method ::getConfigForPackage
549
        if (empty($rule_object->config)) {
3✔
550
            return $this;
1✔
551
        }
552
        $new_config = clone $this->config;
2✔
553
        $this->mergeConfigFromConfigObject($new_config, $rule_object->config, true);
2✔
554
        return self::createFromViolinistConfig($new_config);
2✔
555
    }
556

557
    public function getConfigForPackage(string $package_name) : self
558
    {
559
        // @todo: Consider de-duplicating with the method above
560
        // (::getConfigForRuleObject).
561
        $rules = $this->getRules();
4✔
562
        if (empty($rules)) {
4✔
563
            return $this;
1✔
564
        }
565
        $new_config = clone $this->config;
3✔
566
        foreach ($this->config->rules as $rule) {
3✔
567
            if (empty($rule->config)) {
3✔
568
                continue;
1✔
569
            }
570
            $matches = $this->getMatcherFactory()->hasMatches($rule, $package_name);
2✔
571
            if (!$matches) {
2✔
572
                continue;
2✔
573
            }
574
            // Then merge the config for this rule.
575
            $this->mergeConfigFromConfigObject($new_config, $rule->config, true);
2✔
576
        }
577
        return self::createFromViolinistConfig($new_config);
3✔
578
    }
579

580
    public function getRules() : array
581
    {
582
        if (!empty($this->config->rules)) {
4✔
583
            return $this->config->rules;
3✔
584
        }
585
        return [];
1✔
586
    }
587

588
    protected function mergeConfig(Config $other, $extends_name, $parent)
589
    {
590
        $keys_and_values_affected = $this->mergeConfigFromConfigObject($this->getConfig(), $other->getConfig());
8✔
591
        $this->extendsStorage->addExtendItems($other->getExtendsStorage()->getExtendItems());
8✔
592
        foreach ($keys_and_values_affected as $key => $value) {
8✔
593
            $this->configOptionsSet[$key] = true;
7✔
594
            $this->extendsStorage->addExtendItem(new ExtendsChainItem($extends_name, $key, $value));
7✔
595
        }
596
    }
597

598
    protected function mergeConfigFromConfigObject(\stdClass $config, \stdClass $other, bool $force = false) : array
599
    {
600
        $affected = [];
11✔
601
        $default_config = $this->getDefaultConfig();
11✔
602
        foreach ($other as $key => $value) {
11✔
603
            if (!$force) {
11✔
604
                // If the value corresponds to the default config, we don't need to
605
                // set it.
606
                if (isset($default_config->{$key}) && $default_config->{$key} === $value) {
8✔
607
                    continue;
8✔
608
                }
609
                // This special case is because the default config is a stdclass,
610
                // and that will not pass the strict equal test. So let's just
611
                // loosen it up a bit for this specific case.
612
                if ($key === 'bundled_packages' && $default_config->{$key} == $value) {
8✔
613
                    continue;
8✔
614
                }
615
                // If our option is set, but not set to the default, let's not merge
616
                // it.
617
                if (isset($default_config->{$key}) && isset($config->{$key})) {
7✔
618
                    // Special case for bundled packages again.
619
                    if ($key === 'bundled_packages') {
5✔
620
                        if ($config->{$key} != $default_config->{$key}) {
5✔
NEW
UNCOV
621
                            continue;
×
622
                        }
623
                    } else {
624
                        if ($config->{$key} !== $default_config->{$key}) {
5✔
625
                            continue;
1✔
626
                        }
627
                    }
628
                }
629
            }
630
            $config->{$key} = $value;
10✔
631
            $affected[$key] = $value;
10✔
632
        }
633
        return $affected;
11✔
634
    }
635

636
    public function getMatcherFactory() : MatcherFactory
637
    {
638
        if (!$this->matcherFactory) {
2✔
639
            $this->matcherFactory = new MatcherFactory();
2✔
640
        }
641
        return $this->matcherFactory;
2✔
642
    }
643
}
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