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

violinist-dev / violinist-config / 24099225034

07 Apr 2026 07:02PM UTC coverage: 99.71% (+0.007%) from 99.703%
24099225034

Pull #46

github

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

23 of 23 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

344 of 345 relevant lines covered (99.71%)

40.25 hits per line

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

99.66
/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();
223✔
18
        $this->extendsStorage = new ExtendsStorage();
223✔
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) [
223✔
93
            'always_update_all' => 0,
223✔
94
            'always_allow_direct_dependencies' => 0,
223✔
95
            'ignore_platform_requirements' => 0,
223✔
96
            'allow_list' => [],
223✔
97
            'update_dev_dependencies' => 1,
223✔
98
            'check_only_direct_dependencies' => 1,
223✔
99
            'composer_outdated_flag' => 'minor',
223✔
100
            'bundled_packages' => (object) [],
223✔
101
            'blocklist' => [],
223✔
102
            'assignees' => [],
223✔
103
            'allow_updates_beyond_constraint' => 1,
223✔
104
            'one_pull_request_per_package' => 0,
223✔
105
            'timeframe_disallowed' => '',
223✔
106
            'timezone' => '+0000',
223✔
107
            'update_with_dependencies' => 1,
223✔
108
            'default_branch' => '',
223✔
109
            'default_branch_security' => '',
223✔
110
            'run_scripts' => 1,
223✔
111
            'security_updates_only' => 0,
223✔
112
            'number_of_concurrent_updates' => 0,
223✔
113
            'allow_security_updates_on_concurrent_limit' => 0,
223✔
114
            'branch_prefix' => '',
223✔
115
            'commit_message_convention' => '',
223✔
116
            'allow_update_indirect_with_direct' => 0,
223✔
117
            'automerge' => 0,
223✔
118
            'automerge_security' => 0,
223✔
119
            'automerge_method' => 'merge',
223✔
120
            'automerge_method_security' => 'merge',
223✔
121
            'labels' => [],
223✔
122
            'labels_security' => [],
223✔
123
        ];
223✔
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();
17✔
221
        $instance->setConfig($data);
17✔
222
        return $instance;
17✔
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
        if (!$config instanceof \stdClass) {
218✔
234
            return;
2✔
235
        }
236
        $config = $this->normalizeConfigKeys($config);
216✔
237
        foreach ($this->getDefaultConfig() as $key => $value) {
216✔
238
            if (isset($config->{$key})) {
216✔
239
                $this->config->{$key} = $config->{$key};
179✔
240
                $this->configOptionsSet[$key] = true;
179✔
241
            }
242
        }
243
        if (!empty($config->rules)) {
216✔
244
            $this->config->rules = $config->rules;
10✔
245
        }
246
    }
247

248
    private function normalizeConfigKeys(\stdClass $config) : \stdClass
249
    {
250
        $config = clone $config;
217✔
251
        $renamed_and_aliased = [
217✔
252
            'blacklist' => 'blocklist',
217✔
253
            'block_list' => 'blocklist',
217✔
254
            'allowlist' => 'allow_list',
217✔
255
        ];
217✔
256
        foreach ($renamed_and_aliased as $not_real => $real) {
217✔
257
            if (isset($config->{$not_real})) {
217✔
258
                $config->{$real} = $config->{$not_real};
6✔
259
                unset($config->{$not_real});
6✔
260
            }
261
        }
262
        return $config;
217✔
263
    }
264

265
    public function getComposerOutdatedFlag() : string
266
    {
267
        if (empty($this->config->composer_outdated_flag)) {
7✔
268
            return 'minor';
2✔
269
        }
270
        $allowed_values = [
5✔
271
            'major',
5✔
272
            'minor',
5✔
273
            'patch',
5✔
274
            'major-only',
5✔
275
        ];
5✔
276
        if (!in_array($this->config->composer_outdated_flag, $allowed_values, true)) {
5✔
277
            return 'minor';
1✔
278
        }
279
        return $this->config->composer_outdated_flag;
4✔
280
    }
281

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

290
    public function getLabelsSecurity() : array
291
    {
292
        if (!is_array($this->config->labels_security)) {
7✔
293
            return [];
3✔
294
        }
295
        return $this->config->labels_security;
4✔
296
    }
297

298
    public function shouldAlwaysAllowDirect() : bool
299
    {
300
        return (bool) $this->config->always_allow_direct_dependencies;
6✔
301
    }
302

303
    public function hasConfigForKey($key)
304
    {
305
        return !empty($this->configOptionsSet[$key]);
28✔
306
    }
307

308
    public function shouldAutoMerge($is_security_update = false)
309
    {
310
        if (!$is_security_update) {
15✔
311
            // It's not a security update. Let's use the option found in the config.
312
            return (bool) $this->config->automerge;
6✔
313
        }
314
        if ($this->shouldAutoMergeSecurity()) {
9✔
315
            // Meaning we should automerge, no matter what the general automerge config says.
316
            return true;
2✔
317
        }
318
        // Fall back to using the actual option.
319
        return (bool) $this->config->automerge;
7✔
320
    }
321

322
    public function getAutomergeMethod($is_security_update = false) : string
323
    {
324
        if (!$is_security_update) {
27✔
325
            return $this->getAutoMergeMethodWithFallback('automerge_method');
13✔
326
        }
327
        // Otherwise, let's see if it's even set in config. Otherwise this
328
        // should be set to the value (or fallback value) of the general
329
        // automerge method.
330
        if ($this->hasConfigForKey('automerge_method_security')) {
14✔
331
            return $this->getAutoMergeMethodWithFallback('automerge_method_security');
6✔
332
        }
333
        return $this->getAutoMergeMethodWithFallback('automerge_method');
8✔
334
    }
335

336
    protected function getAutoMergeMethodWithFallback($automerge_property) : string
337
    {
338
        if (!in_array($this->config->{$automerge_property}, [
27✔
339
            'merge',
27✔
340
            'rebase',
27✔
341
            'squash',
27✔
342
        ])
27✔
343
        ) {
344
            return 'merge';
9✔
345
        }
346
        return $this->config->{$automerge_property};
18✔
347
    }
348

349
    public function shouldAutoMergeSecurity()
350
    {
351
        return (bool) $this->config->automerge_security;
9✔
352
    }
353

354
    public function shouldUpdateIndirectWithDirect()
355
    {
356
        return (bool) $this->config->allow_update_indirect_with_direct;
4✔
357
    }
358

359
    public function shouldAlwaysUpdateAll()
360
    {
361
        return (bool) $this->config->always_update_all;
6✔
362
    }
363

364
    public function getTimeZone()
365
    {
366
        if (!is_string($this->config->timezone)) {
5✔
367
            return '+0000';
2✔
368
        }
369
        if (empty($this->config->timezone)) {
3✔
370
            return '+0000';
1✔
371
        }
372
        return $this->config->timezone;
2✔
373
    }
374

375
    public function getTimeFrameDisallowed()
376
    {
377
        if (!is_string($this->config->timeframe_disallowed)) {
5✔
378
            return '';
1✔
379
        }
380
        if (empty($this->config->timeframe_disallowed)) {
4✔
381
            return '';
2✔
382
        }
383
        $frame = $this->config->timeframe_disallowed;
2✔
384
        $length = count(explode('-', $frame));
2✔
385
        if ($length !== 2) {
2✔
386
            throw new \InvalidArgumentException('The timeframe should consist of two 24 hour format times separated by a dash ("-")');
1✔
387
        }
388
        return $this->config->timeframe_disallowed;
1✔
389
    }
390

391
    public function shouldUpdateWithDependencies()
392
    {
393
        return (bool) $this->config->update_with_dependencies;
5✔
394
    }
395

396
    public function shouldAllowUpdatesBeyondConstraint()
397
    {
398
        return (bool) $this->config->allow_updates_beyond_constraint;
5✔
399
    }
400

401
    public function shouldRunScripts()
402
    {
403
        return (bool) $this->config->run_scripts;
8✔
404
    }
405

406
    public function getPackagesWithBundles()
407
    {
408
        $with_bundles = [];
7✔
409
        if (!is_object($this->config->bundled_packages)) {
7✔
410
            return [];
2✔
411
        }
412
        foreach ($this->config->bundled_packages as $package => $bundle) {
5✔
413
            if (!is_array($bundle)) {
3✔
414
                continue;
2✔
415
            }
416
            $with_bundles[] = $package;
1✔
417
        }
418
        return $with_bundles;
5✔
419
    }
420

421
    public function getBundledPackagesForPackage($package_name)
422
    {
423
        if (!is_object($this->config->bundled_packages)) {
8✔
424
            return [];
2✔
425
        }
426
        foreach ($this->config->bundled_packages as $package => $bundle) {
6✔
427
            if ($package === $package_name) {
5✔
428
                if (!is_array($bundle)) {
5✔
429
                    throw new \Exception('Found bundle for ' . $package . ' but the bundle was not an array');
2✔
430
                }
431
                return $bundle;
3✔
432
            }
433
        }
434
        return [];
1✔
435
    }
436

437
    public function getAssignees()
438
    {
439
        if (!is_array($this->config->assignees)) {
4✔
440
            return [];
1✔
441
        }
442

443
        return $this->config->assignees;
3✔
444
    }
445

446
    /**
447
     * Just an alias, since providers differ in their wording on this.
448
     */
449
    public function shouldUseOneMergeRequestPerPackage()
450
    {
451
        return $this->shouldUseOnePullRequestPerPackage();
5✔
452
    }
453

454
    public function shouldUseOnePullRequestPerPackage()
455
    {
456
        return (bool) $this->config->one_pull_request_per_package;
5✔
457
    }
458

459
    public function getBlockList()
460
    {
461
        if (!is_array($this->config->blocklist)) {
10✔
462
            return [];
2✔
463
        }
464

465
        return $this->config->blocklist;
8✔
466
    }
467

468
    public function getAllowList()
469
    {
470
        if (!is_array($this->config->allow_list)) {
4✔
471
            return [];
1✔
472
        }
473

474
        return $this->config->allow_list;
3✔
475
    }
476

477
    /**
478
     * @deprecated Use ::getBlockList instead.
479
     */
480
    public function getBlackList()
481
    {
482
        return $this->getBlockList();
8✔
483
    }
484

485
    public function shouldUpdateDevDependencies()
486
    {
487
        return (bool) $this->config->update_dev_dependencies;
14✔
488
    }
489

490
    public function getNumberOfAllowedPrs()
491
    {
492
        return (int) $this->config->number_of_concurrent_updates;
6✔
493
    }
494

495
    public function shouldAllowSecurityUpdatesOnConcurrentLimit()
496
    {
497
        return (bool) $this->config->allow_security_updates_on_concurrent_limit;
3✔
498
    }
499

500
    public function shouldOnlyUpdateSecurityUpdates()
501
    {
502
        return (bool) $this->config->security_updates_only;
11✔
503
    }
504

505
    public function getDefaultBranchSecurity()
506
    {
507
        return $this->getDefaultBranch(true);
7✔
508
    }
509

510
    public function getDefaultBranch($is_security = false)
511
    {
512
        if ($is_security && !empty($this->config->default_branch_security)) {
16✔
513
            return $this->config->default_branch_security;
2✔
514
        }
515
        if ($is_security && empty($this->config->default_branch_security)) {
14✔
516
            return $this->getDefaultBranch();
5✔
517
        }
518
        if (empty($this->config->default_branch)) {
14✔
519
            return false;
11✔
520
        }
521
        return $this->config->default_branch;
3✔
522
    }
523

524
    public function shouldCheckDirectOnly()
525
    {
526
        return (bool) $this->config->check_only_direct_dependencies;
8✔
527
    }
528

529
    public function getBranchPrefix()
530
    {
531
        if ($this->config->branch_prefix) {
6✔
532
            if (!is_string($this->config->branch_prefix)) {
4✔
533
                return '';
1✔
534
            }
535
            return (string) $this->config->branch_prefix;
3✔
536
        }
537
        return '';
2✔
538
    }
539

540
    public function shouldIgnorePlatformRequirements() : bool
541
    {
542
        return (bool) $this->config->ignore_platform_requirements;
6✔
543
    }
544

545
    public function getCommitMessageConvention()
546
    {
547
        if (!$this->config->commit_message_convention || !is_string($this->config->commit_message_convention)) {
4✔
548
            return '';
3✔
549
        }
550

551
        return $this->config->commit_message_convention;
1✔
552
    }
553

554
    public function getConfigForRuleObject(\stdClass $rule_object)
555
    {
556
        if (empty($rule_object->config)) {
13✔
557
            return $this;
2✔
558
        }
559
        $clone = clone $this;
11✔
560
        $clone->config = clone $this->config;
11✔
561
        foreach ($this->normalizeConfigKeys($rule_object->config) as $key => $value) {
11✔
562
            $clone->config->{$key} = $value;
11✔
563
            $clone->configOptionsSet[$key] = true;
11✔
564
        }
565
        return $clone;
11✔
566
    }
567

568
    public function getConfigForPackage(string $package_name) : self
569
    {
570
        $rules = $this->getRules();
10✔
571
        if (empty($rules)) {
10✔
572
            return $this;
1✔
573
        }
574
        $result = $this;
9✔
575
        foreach ($rules as $rule) {
9✔
576
            if (!$this->getMatcherFactory()->hasMatches($rule, $package_name)) {
9✔
577
                continue;
4✔
578
            }
579
            $result = $result->getConfigForRuleObject($rule);
9✔
580
        }
581
        return $result;
9✔
582
    }
583

584
    public function getRules() : array
585
    {
586
        if (!empty($this->config->rules)) {
10✔
587
            return $this->config->rules;
9✔
588
        }
589
        return [];
1✔
590
    }
591

592
    protected function mergeConfig(Config $other, $extends_name, $parent)
593
    {
594
        $keys_and_values_affected = $this->mergeConfigFromConfigObject($this->getConfig(), $other->getConfig());
8✔
595
        $this->extendsStorage->addExtendItems($other->getExtendsStorage()->getExtendItems());
8✔
596
        foreach ($this->getDefaultConfig() as $key => $value) {
8✔
597
            if ($other->hasConfigForKey($key)) {
8✔
598
                $this->configOptionsSet[$key] = true;
8✔
599
            }
600
        }
601
        foreach ($keys_and_values_affected as $key => $value) {
8✔
602
            $this->extendsStorage->addExtendItem(new ExtendsChainItem($extends_name, $key, $value));
7✔
603
        }
604
    }
605

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

642
    public function getMatcherFactory() : MatcherFactory
643
    {
644
        if (!$this->matcherFactory) {
9✔
645
            $this->matcherFactory = new MatcherFactory();
9✔
646
        }
647
        return $this->matcherFactory;
9✔
648
    }
649
}
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