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

violinist-dev / violinist-config / 14410896303

11 Apr 2025 07:47PM UTC coverage: 98.859% (-1.1%) from 100.0%
14410896303

Pull #36

github

eiriksm
Make sure multi level extends work intuitively
Pull Request #36: Make sure multi level extends work intuitively

12 of 15 new or added lines in 1 file covered. (80.0%)

2 existing lines in 1 file now uncovered.

260 of 263 relevant lines covered (98.86%)

41.62 hits per line

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

98.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

11
    const VIOLINIST_CONFIG_FILE = 'violinist-config.json';
12

13
    public function __construct()
14
    {
15
        $this->config = $this->getDefaultConfig();
205✔
16
    }
17

18
    public function getDefaultConfig()
19
    {
20
        return (object) [
205✔
21
            'always_update_all' => 0,
205✔
22
            'always_allow_direct_dependencies' => 0,
205✔
23
            'ignore_platform_requirements' => 0,
205✔
24
            'allow_list' => [],
205✔
25
            'update_dev_dependencies' => 1,
205✔
26
            'check_only_direct_dependencies' => 1,
205✔
27
            'composer_outdated_flag' => 'minor',
205✔
28
            'bundled_packages' => (object) [],
205✔
29
            'blocklist' => [],
205✔
30
            'assignees' => [],
205✔
31
            'allow_updates_beyond_constraint' => 1,
205✔
32
            'one_pull_request_per_package' => 0,
205✔
33
            'timeframe_disallowed' => '',
205✔
34
            'timezone' => '+0000',
205✔
35
            'update_with_dependencies' => 1,
205✔
36
            'default_branch' => '',
205✔
37
            'default_branch_security' => '',
205✔
38
            'run_scripts' => 1,
205✔
39
            'security_updates_only' => 0,
205✔
40
            'number_of_concurrent_updates' => 0,
205✔
41
            'allow_security_updates_on_concurrent_limit' => 0,
205✔
42
            'branch_prefix' => '',
205✔
43
            'commit_message_convention' => '',
205✔
44
            'allow_update_indirect_with_direct' => 0,
205✔
45
            'automerge' => 0,
205✔
46
            'automerge_security' => 0,
205✔
47
            'automerge_method' => 'merge',
205✔
48
            'automerge_method_security' => 'merge',
205✔
49
            'labels' => [],
205✔
50
            'labels_security' => [],
205✔
51
        ];
205✔
52
    }
53

54
    public static function createFromComposerPath(string $path)
55
    {
56
        if (!file_exists($path)) {
6✔
57
            throw new \InvalidArgumentException('The path provided does not contain a composer.json file');
1✔
58
        }
59
        $composer_data = json_decode(file_get_contents($path));
5✔
60
        return self::createFromComposerDataInPath($composer_data, $path);
5✔
61
    }
62

63
    public static function createFromComposerDataInPath(\stdClass $data, string $path, string $initial_path = null)
64
    {
65
        // First we need the actual thing from the composer data.
66
        $instance = self::createFromComposerData($data);
5✔
67
        $extra_data = (object) [];
5✔
68
        if (!empty($data->extra->violinist)) {
5✔
69
            $extra_data = $data->extra->violinist;
5✔
70
        }
71
        $instance = self::handleExtendFromInstanceAndData($instance, $extra_data, $path, $initial_path);
5✔
72
        return $instance;
5✔
73
    }
74

75
    public static function handleExtendFromInstanceAndData(Config $instance, $data, $path, $initial_path = null) : Config
76
    {
77
        if (!$initial_path) {
5✔
78
            $initial_path = dirname($path);
5✔
79
        }
80
        // Now, is there a thing on the path in extends? Is there even
81
        // "extends"?
82
        if (empty($data->extends)) {
5✔
83
            return $instance;
5✔
84
        }
85
        $extends = $data->extends;
4✔
86
        // Remove the filename part of the path.
87
        $directory = dirname($path);
4✔
88
        $extends_path = $directory . '/' . $extends;
4✔
89
        $potential_places = [
4✔
90
            $extends_path,
4✔
91
            sprintf('%s/vendor/%s/%s', $directory, $extends, self::VIOLINIST_CONFIG_FILE),
4✔
92
            sprintf('%s/vendor/%s/composer.json', $directory, $extends),
4✔
93
            sprintf('%s/vendor/%s', $directory, $extends),
4✔
94
        ];
4✔
95
        if ($initial_path) {
4✔
96
            $potential_places[] = sprintf('%s/vendor/%s/%s', $initial_path, $extends, self::VIOLINIST_CONFIG_FILE);
4✔
97
            $potential_places[] = "$initial_path/vendor/$extends/composer.json";
4✔
98
        }
99
        foreach ($potential_places as $potential_place) {
4✔
100
            if (file_exists($potential_place)) {
4✔
101
                $extends_data = json_decode(file_get_contents($potential_place));
4✔
102
                if (!$extends_data) {
4✔
NEW
103
                    continue;
×
104
                }
105
                $extends_instance = self::createFromViolinistConfigInPath($extends_data, $potential_place, $initial_path);
4✔
106
                if (strpos($potential_place, 'composer.json') !== false) {
4✔
107
                    // This is a composer.json file. Let's remove the filename,
108
                    // and pass it in here instead.
109
                    try {
110
                        $extends_instance = self::createFromComposerDataInPath($extends_data, $potential_place, $initial_path);
1✔
NEW
UNCOV
111
                    } catch (\Throwable $e) {
×
112
                        // Not goind to work very well, is it.
NEW
UNCOV
113
                        continue;
×
114
                    }
115
                }
116
                // Now merge the two.
117
                $instance->mergeConfig($instance->config, $extends_instance->config);
4✔
118
                break;
4✔
119
            }
120
        }
121
        return $instance;
4✔
122
    }
123

124
    public static function createFromComposerData($data)
125
    {
126
        $instance = new self();
205✔
127
        if (!empty($data->extra->violinist)) {
205✔
128
            $instance->setConfig($data->extra->violinist);
202✔
129
        }
130
        return $instance;
205✔
131
    }
132

133
    public static function createFromViolinistConfigInPath($data, $file_path, $initial_path = null)
134
    {
135
        $instance = self::createFromViolinistConfig($data);
4✔
136
        $instance = self::handleExtendFromInstanceAndData($instance, $data, $file_path, $initial_path);
4✔
137
        return $instance;
4✔
138
    }
139

140
    public static function createFromViolinistConfig($data)
141
    {
142
        $instance = new self();
4✔
143
        $instance->setConfig($data);
4✔
144
        return $instance;
4✔
145
    }
146

147
    public static function createFromViolinistConfigJsonString(string $data)
148
    {
149
        $json_data = json_decode($data, false, 512, JSON_THROW_ON_ERROR);
3✔
150
        return self::createFromViolinistConfig($json_data);
3✔
151
    }
152

153
    public function setConfig($config)
154
    {
155
        foreach ($this->getDefaultConfig() as $key => $value) {
202✔
156
            if (isset($config->{$key})) {
202✔
157
                $this->config->{$key} = $config->{$key};
159✔
158
                $this->configOptionsSet[$key] = true;
159✔
159
            }
160
        }
161
        // Also make sure to set the block list config from the deprecated part.
162
        // Plus alternative spelling from allow list.
163
        $renamed_and_aliased = [
202✔
164
            'blacklist' => 'blocklist',
202✔
165
            'block_list' => 'blocklist',
202✔
166
            'allowlist' => 'allow_list',
202✔
167
        ];
202✔
168
        foreach ($renamed_and_aliased as $not_real => $real) {
202✔
169
            if (isset($config->{$not_real})) {
202✔
170
                $this->config->{$real} = $config->{$not_real};
5✔
171
            }
172
        }
173
        if (!empty($config->rules)) {
202✔
174
            $this->config->rules = $config->rules;
2✔
175
        }
176
    }
177

178
    public function getComposerOutdatedFlag() : string
179
    {
180
        if (empty($this->config->composer_outdated_flag)) {
5✔
181
            return 'minor';
1✔
182
        }
183
        $allowed_values = [
4✔
184
            'major',
4✔
185
            'minor',
4✔
186
            'patch',
4✔
187
        ];
4✔
188
        if (!in_array($this->config->composer_outdated_flag, $allowed_values)) {
4✔
189
            return 'minor';
1✔
190
        }
191
        return $this->config->composer_outdated_flag;
3✔
192
    }
193

194
    public function getLabels() : array
195
    {
196
        if (!is_array($this->config->labels)) {
7✔
197
            return [];
3✔
198
        }
199
        return $this->config->labels;
4✔
200
    }
201

202
    public function getLabelsSecurity() : array
203
    {
204
        if (!is_array($this->config->labels_security)) {
7✔
205
            return [];
3✔
206
        }
207
        return $this->config->labels_security;
4✔
208
    }
209

210
    public function shouldAlwaysAllowDirect() : bool
211
    {
212
        return (bool) $this->config->always_allow_direct_dependencies;
6✔
213
    }
214

215
    public function hasConfigForKey($key)
216
    {
217
        return !empty($this->configOptionsSet[$key]);
18✔
218
    }
219

220
    public function shouldAutoMerge($is_security_update = false)
221
    {
222
        if (!$is_security_update) {
15✔
223
            // It's not a security update. Let's use the option found in the config.
224
            return (bool) $this->config->automerge;
6✔
225
        }
226
        if ($this->shouldAutoMergeSecurity()) {
9✔
227
            // Meaning we should automerge, no matter what the general automerge config says.
228
            return true;
2✔
229
        }
230
        // Fall back to using the actual option.
231
        return (bool) $this->config->automerge;
7✔
232
    }
233

234
    public function getAutomergeMethod($is_security_update = false) : string
235
    {
236
        if (!$is_security_update) {
26✔
237
            return $this->getAutoMergeMethodWithFallback('automerge_method');
13✔
238
        }
239
        // Otherwise, let's see if it's even set in config. Otherwise this
240
        // should be set to the value (or fallback value) of the general
241
        // automerge method.
242
        if ($this->hasConfigForKey('automerge_method_security')) {
13✔
243
            return $this->getAutoMergeMethodWithFallback('automerge_method_security');
6✔
244
        }
245
        return $this->getAutoMergeMethodWithFallback('automerge_method');
7✔
246
    }
247

248
    protected function getAutoMergeMethodWithFallback($automerge_property) : string
249
    {
250
        if (!in_array($this->config->{$automerge_property}, [
26✔
251
            'merge',
26✔
252
            'rebase',
26✔
253
            'squash',
26✔
254
        ])
26✔
255
        ) {
256
            return 'merge';
9✔
257
        }
258
        return $this->config->{$automerge_property};
17✔
259
    }
260

261
    public function shouldAutoMergeSecurity()
262
    {
263
        return (bool) $this->config->automerge_security;
9✔
264
    }
265

266
    public function shouldUpdateIndirectWithDirect()
267
    {
268
        return (bool) $this->config->allow_update_indirect_with_direct;
4✔
269
    }
270

271
    public function shouldAlwaysUpdateAll()
272
    {
273
        return (bool) $this->config->always_update_all;
6✔
274
    }
275

276
    public function getTimeZone()
277
    {
278
        if (!is_string($this->config->timezone)) {
5✔
279
            return '+0000';
2✔
280
        }
281
        if (empty($this->config->timezone)) {
3✔
282
            return '+0000';
1✔
283
        }
284
        return $this->config->timezone;
2✔
285
    }
286

287
    public function getTimeFrameDisallowed()
288
    {
289
        if (!is_string($this->config->timeframe_disallowed)) {
5✔
290
            return '';
1✔
291
        }
292
        if (empty($this->config->timeframe_disallowed)) {
4✔
293
            return '';
2✔
294
        }
295
        $frame = $this->config->timeframe_disallowed;
2✔
296
        $length = count(explode('-', $frame));
2✔
297
        if ($length !== 2) {
2✔
298
            throw new \InvalidArgumentException('The timeframe should consist of two 24 hour format times separated by a dash ("-")');
1✔
299
        }
300
        return $this->config->timeframe_disallowed;
1✔
301
    }
302

303
    public function shouldUpdateWithDependencies()
304
    {
305
        return (bool) $this->config->update_with_dependencies;
5✔
306
    }
307

308
    public function shouldAllowUpdatesBeyondConstraint()
309
    {
310
        return (bool) $this->config->allow_updates_beyond_constraint;
5✔
311
    }
312

313
    public function shouldRunScripts()
314
    {
315
        return (bool) $this->config->run_scripts;
8✔
316
    }
317

318
    public function getPackagesWithBundles()
319
    {
320
        $with_bundles = [];
7✔
321
        if (!is_object($this->config->bundled_packages)) {
7✔
322
            return [];
2✔
323
        }
324
        foreach ($this->config->bundled_packages as $package => $bundle) {
5✔
325
            if (!is_array($bundle)) {
3✔
326
                continue;
2✔
327
            }
328
            $with_bundles[] = $package;
1✔
329
        }
330
        return $with_bundles;
5✔
331
    }
332

333
    public function getBundledPackagesForPackage($package_name)
334
    {
335
        if (!is_object($this->config->bundled_packages)) {
6✔
336
            return [];
2✔
337
        }
338
        foreach ($this->config->bundled_packages as $package => $bundle) {
4✔
339
            if ($package === $package_name) {
3✔
340
                if (!is_array($bundle)) {
3✔
341
                    throw new \Exception('Found bundle for ' . $package . ' but the bundle was not an array');
2✔
342
                }
343
                return $bundle;
1✔
344
            }
345
        }
346
        return [];
1✔
347
    }
348

349
    public function getAssignees()
350
    {
351
        if (!is_array($this->config->assignees)) {
4✔
352
            return [];
1✔
353
        }
354

355
        return $this->config->assignees;
3✔
356
    }
357

358
    /**
359
     * Just an alias, since providers differ in their wording on this.
360
     */
361
    public function shouldUseOneMergeRequestPerPackage()
362
    {
363
        return $this->shouldUseOnePullRequestPerPackage();
5✔
364
    }
365

366
    public function shouldUseOnePullRequestPerPackage()
367
    {
368
        return (bool) $this->config->one_pull_request_per_package;
5✔
369
    }
370

371
    public function getBlockList()
372
    {
373
        if (!is_array($this->config->blocklist)) {
8✔
374
            return [];
2✔
375
        }
376

377
        return $this->config->blocklist;
6✔
378
    }
379

380
    public function getAllowList()
381
    {
382
        if (!is_array($this->config->allow_list)) {
4✔
383
            return [];
1✔
384
        }
385

386
        return $this->config->allow_list;
3✔
387
    }
388

389
    /**
390
     * @deprecated Use ::getBlockList instead.
391
     */
392
    public function getBlackList()
393
    {
394
        return $this->getBlockList();
8✔
395
    }
396

397
    public function shouldUpdateDevDependencies()
398
    {
399
        return (bool) $this->config->update_dev_dependencies;
11✔
400
    }
401

402
    public function getNumberOfAllowedPrs()
403
    {
404
        return (int) $this->config->number_of_concurrent_updates;
6✔
405
    }
406

407
    public function shouldAllowSecurityUpdatesOnConcurrentLimit()
408
    {
409
        return (bool) $this->config->allow_security_updates_on_concurrent_limit;
3✔
410
    }
411

412
    public function shouldOnlyUpdateSecurityUpdates()
413
    {
414
        return (bool) $this->config->security_updates_only;
8✔
415
    }
416

417
    public function getDefaultBranchSecurity()
418
    {
419
        return $this->getDefaultBranch(true);
7✔
420
    }
421

422
    public function getDefaultBranch($is_security = false)
423
    {
424
        if ($is_security && !empty($this->config->default_branch_security)) {
16✔
425
            return $this->config->default_branch_security;
2✔
426
        }
427
        if ($is_security && empty($this->config->default_branch_security)) {
14✔
428
            return $this->getDefaultBranch();
5✔
429
        }
430
        if (empty($this->config->default_branch)) {
14✔
431
            return false;
11✔
432
        }
433
        return $this->config->default_branch;
3✔
434
    }
435

436
    public function shouldCheckDirectOnly()
437
    {
438
        return (bool) $this->config->check_only_direct_dependencies;
8✔
439
    }
440

441
    public function getBranchPrefix()
442
    {
443
        if ($this->config->branch_prefix) {
5✔
444
            if (!is_string($this->config->branch_prefix)) {
3✔
445
                return '';
1✔
446
            }
447
            return (string) $this->config->branch_prefix;
2✔
448
        }
449
        return '';
2✔
450
    }
451

452
    public function shouldIgnorePlatformRequirements() : bool
453
    {
454
        return (bool) $this->config->ignore_platform_requirements;
6✔
455
    }
456

457
    public function getCommitMessageConvention()
458
    {
459
        if (!$this->config->commit_message_convention || !is_string($this->config->commit_message_convention)) {
4✔
460
            return '';
3✔
461
        }
462

463
        return $this->config->commit_message_convention;
1✔
464
    }
465

466
    public function getConfigForPackage(string $package_name) : self
467
    {
468
        $rules = $this->getRules();
3✔
469
        if (empty($rules)) {
3✔
470
            return $this;
1✔
471
        }
472
        $new_config = clone $this->config;
2✔
473
        foreach ($this->config->rules as $rule) {
2✔
474
            if (empty($rule->config)) {
2✔
475
                continue;
1✔
476
            }
477
            $matches = $this->getMatcherFactory()->hasMatches($rule, $package_name);
1✔
478
            if (!$matches) {
1✔
479
                continue;
1✔
480
            }
481
            // Then merge the config for this rule.
482
            $this->mergeConfig($new_config, $rule->config);
1✔
483
        }
484
        return self::createFromViolinistConfig($new_config);
2✔
485
    }
486

487
    public function getRules() : array
488
    {
489
        if (!empty($this->config->rules)) {
3✔
490
            return $this->config->rules;
2✔
491
        }
492
        return [];
1✔
493
    }
494

495
    protected function mergeConfig(\stdClass $config, \stdClass $other)
496
    {
497
        foreach ($other as $key => $value) {
4✔
498
            $config->{$key} = $value;
4✔
499
        }
500
    }
501

502
    public function getMatcherFactory() : MatcherFactory
503
    {
504
        if (!$this->matcherFactory) {
1✔
505
            $this->matcherFactory = new MatcherFactory();
1✔
506
        }
507
        return $this->matcherFactory;
1✔
508
    }
509
}
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