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

violinist-dev / violinist-config / 14551699093

19 Apr 2025 06:17PM UTC coverage: 99.083% (-0.9%) from 100.0%
14551699093

Pull #41

github

eiriksm
Also test non extended
Pull Request #41: Add methods for finding the correct extend responsible for setting things

63 of 66 new or added lines in 3 files covered. (95.45%)

1 existing line in 1 file now uncovered.

324 of 327 relevant lines covered (99.08%)

35.92 hits per line

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

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

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

126
    public static function createFromComposerPath(string $path)
127
    {
128
        if (!file_exists($path)) {
10✔
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));
9✔
132
        return self::createFromComposerDataInPath($composer_data, $path);
9✔
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);
9✔
139
        $extra_data = (object) [];
9✔
140
        if (!empty($data->extra->violinist)) {
9✔
141
            $extra_data = $data->extra->violinist;
9✔
142
        }
143
        $instance = self::handleExtendFromInstanceAndData($instance, $extra_data, $path, $initial_path, $parent);
9✔
144
        return $instance;
9✔
145
    }
146

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

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

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

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

218
    public static function createFromViolinistConfig($data)
219
    {
220
        $instance = new self();
7✔
221
        $instance->setConfig($data);
7✔
222
        return $instance;
7✔
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) {
206✔
234
            if (isset($config->{$key})) {
206✔
235
                $this->config->{$key} = $config->{$key};
163✔
236
                $this->configOptionsSet[$key] = true;
163✔
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 = [
206✔
242
            'blacklist' => 'blocklist',
206✔
243
            'block_list' => 'blocklist',
206✔
244
            'allowlist' => 'allow_list',
206✔
245
        ];
206✔
246
        foreach ($renamed_and_aliased as $not_real => $real) {
206✔
247
            if (isset($config->{$not_real})) {
206✔
248
                $this->config->{$real} = $config->{$not_real};
5✔
249
            }
250
        }
251
        if (!empty($config->rules)) {
206✔
252
            $this->config->rules = $config->rules;
2✔
253
        }
254
    }
255

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

455
        return $this->config->blocklist;
6✔
456
    }
457

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

605
    public function getExtendsChain()
606
    {
NEW
UNCOV
607
        return $this->extendsChain;
×
608
    }
609

610
    public function getMatcherFactory() : MatcherFactory
611
    {
612
        if (!$this->matcherFactory) {
1✔
613
            $this->matcherFactory = new MatcherFactory();
1✔
614
        }
615
        return $this->matcherFactory;
1✔
616
    }
617
}
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