Completed
Push — master ( 645bf6...b9134b )
by Sullivan
02:44
created

Linter::validateVersionConstraints()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 36
rs 8.439
cc 6
eloc 19
nc 7
nop 1
1
<?php
2
3
namespace SLLH\ComposerLint;
4
5
/**
6
 * @author Sullivan Senechal <[email protected]>
7
 */
8
final class Linter
9
{
10
    /**
11
     * @var array
12
     */
13
    private $config;
14
15
    /**
16
     * @param array $config
17
     */
18
    public function __construct(array $config)
19
    {
20
        $defaultConfig = array(
21
            'php' => true,
22
            'type' => true,
23
            'minimum-stability' => true,
24
            'version-constraints' => true,
25
        );
26
27
        $this->config = array_merge($defaultConfig, $config);
28
    }
29
30
    /**
31
     * @param array $manifest composer.json file manifest
32
     *
33
     * @return string[]
34
     */
35
    public function validate($manifest)
36
    {
37
        $errors = array();
38
        $linksSections = array('require', 'require-dev', 'conflict', 'replace', 'provide', 'suggest');
39
40
        if (isset($manifest['config']['sort-packages']) && $manifest['config']['sort-packages']) {
41
            foreach ($linksSections as $linksSection) {
42
                if (array_key_exists($linksSection, $manifest) && !$this->packagesAreSorted($manifest[$linksSection])) {
43
                    array_push($errors, 'Links under '.$linksSection.' section are not sorted.');
44
                }
45
            }
46
        }
47
48
        if (true === $this->config['php'] &&
49
            (array_key_exists('require-dev', $manifest) || array_key_exists('require', $manifest))) {
50
            $isOnRequireDev = array_key_exists('require-dev', $manifest) && array_key_exists('php', $manifest['require-dev']);
51
            $isOnRequire = array_key_exists('require', $manifest) && array_key_exists('php', $manifest['require']);
52
53
            if ($isOnRequireDev) {
54
                array_push($errors, 'PHP requirement should be in the require section, not in the require-dev section.');
55
            } elseif (!$isOnRequire) {
56
                array_push($errors, 'You must specifiy the PHP requirement.');
57
            }
58
        }
59
60
        if (true === $this->config['type'] && !array_key_exists('type', $manifest)) {
61
            array_push($errors, 'The package type is not specified.');
62
        }
63
64
        if (true === $this->config['minimum-stability'] && array_key_exists('minimum-stability', $manifest) &&
65
            array_key_exists('type', $manifest) && 'project' !== $manifest['type']) {
66
            array_push($errors, 'The minimum-stability should be only used for project packages.');
67
        }
68
69
        if (true === $this->config['version-constraints']) {
70
            foreach ($linksSections as $linksSection) {
71
                if (array_key_exists($linksSection, $manifest)) {
72
                    $errors = array_merge($errors, $this->validateVersionConstraints($manifest[$linksSection]));
73
                }
74
            }
75
        }
76
77
        return $errors;
78
    }
79
80
    private function packagesAreSorted(array $packages)
81
    {
82
        $names = array_keys($packages);
83
84
        $hasPHP = in_array('php', $names);
85
        $extNames = array_filter($names, function ($name) {
86
            return 'ext-' === substr($name, 0, 4) && !strstr($name, '/');
87
        });
88
        sort($extNames);
89
        $vendorName = array_filter($names, function ($name) {
90
            return 'ext-' !== substr($name, 0, 4) && 'php' !== $name;
91
        });
92
        sort($vendorName);
93
94
        $sortedNames = array_merge(
95
            $hasPHP ? array('php') : array(),
96
            $extNames,
97
            $vendorName
98
        );
99
100
        return $sortedNames === $names;
101
    }
102
103
    /**
104
     * @param string[] $packages
105
     *
106
     * @return array
107
     */
108
    private function validateVersionConstraints(array $packages)
109
    {
110
        $errors = array();
111
112
        foreach ($packages as $name => $constraint) {
113
            // Checks if OR format is correct
114
            // From Composer\Semver\VersionParser::parseConstraints
115
            $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraint));
116
            foreach ($orConstraints as &$subConstraint) {
117
                // Checks ~ usage
118
                $subConstraint = str_replace('~', '^', $subConstraint);
119
120
                // Checks for usage like ^2.1,>=2.1.5. Should be ^2.1.5.
121
                // From Composer\Semver\VersionParser::parseConstraints
122
                $andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $subConstraint);
123
                if (2 === count($andConstraints) && '>=' === substr($andConstraints[1], 0, 2)) {
124
                    $andConstraints[1] = '^'.substr($andConstraints[1], 2);
125
                    array_shift($andConstraints);
126
                    $subConstraint = implode(',', $andConstraints);
127
                }
128
            }
129
130
            $expectedConstraint = implode(' || ', $orConstraints);
131
132
            if ($expectedConstraint !== $constraint) {
133
                array_push($errors, sprintf(
134
                    "Requirement format of '%s:%s' is not valid. Should be '%s'.",
135
                    $name,
136
                    $constraint,
137
                    $expectedConstraint
138
                ));
139
            }
140
        }
141
142
        return $errors;
143
    }
144
}
145