PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / vendor / composer / semver / src / Intervals.php
matomo / app / vendor / composer / semver / src Last commit date
Constraint 1 year ago Comparator.php 1 year ago CompilingMatcher.php 1 year ago Interval.php 1 year ago Intervals.php 1 year ago Semver.php 7 months ago VersionParser.php 1 year ago
Intervals.php
479 lines
1 <?php
2
3 /*
4 * This file is part of composer/semver.
5 *
6 * (c) Composer <https://github.com/composer>
7 *
8 * For the full copyright and license information, please view
9 * the LICENSE file that was distributed with this source code.
10 */
11
12 namespace Composer\Semver;
13
14 use Composer\Semver\Constraint\Constraint;
15 use Composer\Semver\Constraint\ConstraintInterface;
16 use Composer\Semver\Constraint\MatchAllConstraint;
17 use Composer\Semver\Constraint\MatchNoneConstraint;
18 use Composer\Semver\Constraint\MultiConstraint;
19
20 /**
21 * Helper class generating intervals from constraints
22 *
23 * This contains utilities for:
24 *
25 * - compacting an existing constraint which can be used to combine several into one
26 * by creating a MultiConstraint out of the many constraints you have.
27 *
28 * - checking whether one subset is a subset of another.
29 *
30 * Note: You should call clear to free memoization memory usage when you are done using this class
31 */
32 class Intervals
33 {
34 /**
35 * @phpstan-var array<string, array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}>
36 */
37 private static $intervalsCache = array();
38
39 /**
40 * @phpstan-var array<string, int>
41 */
42 private static $opSortOrder = array(
43 '>=' => -3,
44 '<' => -2,
45 '>' => 2,
46 '<=' => 3,
47 );
48
49 /**
50 * Clears the memoization cache once you are done
51 *
52 * @return void
53 */
54 public static function clear()
55 {
56 self::$intervalsCache = array();
57 }
58
59 /**
60 * Checks whether $candidate is a subset of $constraint
61 *
62 * @return bool
63 */
64 public static function isSubsetOf(ConstraintInterface $candidate, ConstraintInterface $constraint)
65 {
66 if ($constraint instanceof MatchAllConstraint) {
67 return true;
68 }
69
70 if ($candidate instanceof MatchNoneConstraint || $constraint instanceof MatchNoneConstraint) {
71 return false;
72 }
73
74 $intersectionIntervals = self::get(new MultiConstraint(array($candidate, $constraint), true));
75 $candidateIntervals = self::get($candidate);
76 if (\count($intersectionIntervals['numeric']) !== \count($candidateIntervals['numeric'])) {
77 return false;
78 }
79
80 foreach ($intersectionIntervals['numeric'] as $index => $interval) {
81 if (!isset($candidateIntervals['numeric'][$index])) {
82 return false;
83 }
84
85 if ((string) $candidateIntervals['numeric'][$index]->getStart() !== (string) $interval->getStart()) {
86 return false;
87 }
88
89 if ((string) $candidateIntervals['numeric'][$index]->getEnd() !== (string) $interval->getEnd()) {
90 return false;
91 }
92 }
93
94 if ($intersectionIntervals['branches']['exclude'] !== $candidateIntervals['branches']['exclude']) {
95 return false;
96 }
97 if (\count($intersectionIntervals['branches']['names']) !== \count($candidateIntervals['branches']['names'])) {
98 return false;
99 }
100 foreach ($intersectionIntervals['branches']['names'] as $index => $name) {
101 if ($name !== $candidateIntervals['branches']['names'][$index]) {
102 return false;
103 }
104 }
105
106 return true;
107 }
108
109 /**
110 * Checks whether $a and $b have any intersection, equivalent to $a->matches($b)
111 *
112 * @return bool
113 */
114 public static function haveIntersections(ConstraintInterface $a, ConstraintInterface $b)
115 {
116 if ($a instanceof MatchAllConstraint || $b instanceof MatchAllConstraint) {
117 return true;
118 }
119
120 if ($a instanceof MatchNoneConstraint || $b instanceof MatchNoneConstraint) {
121 return false;
122 }
123
124 $intersectionIntervals = self::generateIntervals(new MultiConstraint(array($a, $b), true), true);
125
126 return \count($intersectionIntervals['numeric']) > 0 || $intersectionIntervals['branches']['exclude'] || \count($intersectionIntervals['branches']['names']) > 0;
127 }
128
129 /**
130 * Attempts to optimize a MultiConstraint
131 *
132 * When merging MultiConstraints together they can get very large, this will
133 * compact it by looking at the real intervals covered by all the constraints
134 * and then creates a new constraint containing only the smallest amount of rules
135 * to match the same intervals.
136 *
137 * @return ConstraintInterface
138 */
139 public static function compactConstraint(ConstraintInterface $constraint)
140 {
141 if (!$constraint instanceof MultiConstraint) {
142 return $constraint;
143 }
144
145 $intervals = self::generateIntervals($constraint);
146 $constraints = array();
147 $hasNumericMatchAll = false;
148
149 if (\count($intervals['numeric']) === 1 && (string) $intervals['numeric'][0]->getStart() === (string) Interval::fromZero() && (string) $intervals['numeric'][0]->getEnd() === (string) Interval::untilPositiveInfinity()) {
150 $constraints[] = $intervals['numeric'][0]->getStart();
151 $hasNumericMatchAll = true;
152 } else {
153 $unEqualConstraints = array();
154 for ($i = 0, $count = \count($intervals['numeric']); $i < $count; $i++) {
155 $interval = $intervals['numeric'][$i];
156
157 // if current interval ends with < N and next interval begins with > N we can swap this out for != N
158 // but this needs to happen as a conjunctive expression together with the start of the current interval
159 // and end of next interval, so [>=M, <N] || [>N, <P] => [>=M, !=N, <P] but M/P can be skipped if
160 // they are zero/+inf
161 if ($interval->getEnd()->getOperator() === '<' && $i+1 < $count) {
162 $nextInterval = $intervals['numeric'][$i+1];
163 if ($interval->getEnd()->getVersion() === $nextInterval->getStart()->getVersion() && $nextInterval->getStart()->getOperator() === '>') {
164 // only add a start if we didn't already do so, can be skipped if we're looking at second
165 // interval in [>=M, <N] || [>N, <P] || [>P, <Q] where unEqualConstraints currently contains
166 // [>=M, !=N] already and we only want to add !=P right now
167 if (\count($unEqualConstraints) === 0 && (string) $interval->getStart() !== (string) Interval::fromZero()) {
168 $unEqualConstraints[] = $interval->getStart();
169 }
170 $unEqualConstraints[] = new Constraint('!=', $interval->getEnd()->getVersion());
171 continue;
172 }
173 }
174
175 if (\count($unEqualConstraints) > 0) {
176 // this is where the end of the following interval of a != constraint is added as explained above
177 if ((string) $interval->getEnd() !== (string) Interval::untilPositiveInfinity()) {
178 $unEqualConstraints[] = $interval->getEnd();
179 }
180
181 // count is 1 if entire constraint is just one != expression
182 if (\count($unEqualConstraints) > 1) {
183 $constraints[] = new MultiConstraint($unEqualConstraints, true);
184 } else {
185 $constraints[] = $unEqualConstraints[0];
186 }
187
188 $unEqualConstraints = array();
189 continue;
190 }
191
192 // convert back >= x - <= x intervals to == x
193 if ($interval->getStart()->getVersion() === $interval->getEnd()->getVersion() && $interval->getStart()->getOperator() === '>=' && $interval->getEnd()->getOperator() === '<=') {
194 $constraints[] = new Constraint('==', $interval->getStart()->getVersion());
195 continue;
196 }
197
198 if ((string) $interval->getStart() === (string) Interval::fromZero()) {
199 $constraints[] = $interval->getEnd();
200 } elseif ((string) $interval->getEnd() === (string) Interval::untilPositiveInfinity()) {
201 $constraints[] = $interval->getStart();
202 } else {
203 $constraints[] = new MultiConstraint(array($interval->getStart(), $interval->getEnd()), true);
204 }
205 }
206 }
207
208 $devConstraints = array();
209
210 if (0 === \count($intervals['branches']['names'])) {
211 if ($intervals['branches']['exclude']) {
212 if ($hasNumericMatchAll) {
213 return new MatchAllConstraint;
214 }
215 // otherwise constraint should contain a != operator and already cover this
216 }
217 } else {
218 foreach ($intervals['branches']['names'] as $branchName) {
219 if ($intervals['branches']['exclude']) {
220 $devConstraints[] = new Constraint('!=', $branchName);
221 } else {
222 $devConstraints[] = new Constraint('==', $branchName);
223 }
224 }
225
226 // excluded branches, e.g. != dev-foo are conjunctive with the interval, so
227 // > 2.0 != dev-foo must return a conjunctive constraint
228 if ($intervals['branches']['exclude']) {
229 if (\count($constraints) > 1) {
230 return new MultiConstraint(array_merge(
231 array(new MultiConstraint($constraints, false)),
232 $devConstraints
233 ), true);
234 }
235
236 if (\count($constraints) === 1 && (string)$constraints[0] === (string)Interval::fromZero()) {
237 if (\count($devConstraints) > 1) {
238 return new MultiConstraint($devConstraints, true);
239 }
240 return $devConstraints[0];
241 }
242
243 return new MultiConstraint(array_merge($constraints, $devConstraints), true);
244 }
245
246 // otherwise devConstraints contains a list of == operators for branches which are disjunctive with the
247 // rest of the constraint
248 $constraints = array_merge($constraints, $devConstraints);
249 }
250
251 if (\count($constraints) > 1) {
252 return new MultiConstraint($constraints, false);
253 }
254
255 if (\count($constraints) === 1) {
256 return $constraints[0];
257 }
258
259 return new MatchNoneConstraint;
260 }
261
262 /**
263 * Creates an array of numeric intervals and branch constraints representing a given constraint
264 *
265 * if the returned numeric array is empty it means the constraint matches nothing in the numeric range (0 - +inf)
266 * if the returned branches array is empty it means no dev-* versions are matched
267 * if a constraint matches all possible dev-* versions, branches will contain Interval::anyDev()
268 *
269 * @return array
270 * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}
271 */
272 public static function get(ConstraintInterface $constraint)
273 {
274 $key = (string) $constraint;
275
276 if (!isset(self::$intervalsCache[$key])) {
277 self::$intervalsCache[$key] = self::generateIntervals($constraint);
278 }
279
280 return self::$intervalsCache[$key];
281 }
282
283 /**
284 * @param bool $stopOnFirstValidInterval
285 *
286 * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}
287 */
288 private static function generateIntervals(ConstraintInterface $constraint, $stopOnFirstValidInterval = false)
289 {
290 if ($constraint instanceof MatchAllConstraint) {
291 return array('numeric' => array(new Interval(Interval::fromZero(), Interval::untilPositiveInfinity())), 'branches' => Interval::anyDev());
292 }
293
294 if ($constraint instanceof MatchNoneConstraint) {
295 return array('numeric' => array(), 'branches' => array('names' => array(), 'exclude' => false));
296 }
297
298 if ($constraint instanceof Constraint) {
299 return self::generateSingleConstraintIntervals($constraint);
300 }
301
302 if (!$constraint instanceof MultiConstraint) {
303 throw new \UnexpectedValueException('The constraint passed in should be an MatchAllConstraint, Constraint or MultiConstraint instance, got '.\get_class($constraint).'.');
304 }
305
306 $constraints = $constraint->getConstraints();
307
308 $numericGroups = array();
309 $constraintBranches = array();
310 foreach ($constraints as $c) {
311 $res = self::get($c);
312 $numericGroups[] = $res['numeric'];
313 $constraintBranches[] = $res['branches'];
314 }
315
316 if ($constraint->isDisjunctive()) {
317 $branches = Interval::noDev();
318 foreach ($constraintBranches as $b) {
319 if ($b['exclude']) {
320 if ($branches['exclude']) {
321 // disjunctive constraint, so only exclude what's excluded in all constraints
322 // !=a,!=b || !=b,!=c => !=b
323 $branches['names'] = array_intersect($branches['names'], $b['names']);
324 } else {
325 // disjunctive constraint so exclude all names which are not explicitly included in the alternative
326 // (==b || ==c) || !=a,!=b => !=a
327 $branches['exclude'] = true;
328 $branches['names'] = array_diff($b['names'], $branches['names']);
329 }
330 } else {
331 if ($branches['exclude']) {
332 // disjunctive constraint so exclude all names which are not explicitly included in the alternative
333 // !=a,!=b || (==b || ==c) => !=a
334 $branches['names'] = array_diff($branches['names'], $b['names']);
335 } else {
336 // disjunctive constraint, so just add all the other branches
337 // (==a || ==b) || ==c => ==a || ==b || ==c
338 $branches['names'] = array_merge($branches['names'], $b['names']);
339 }
340 }
341 }
342 } else {
343 $branches = Interval::anyDev();
344 foreach ($constraintBranches as $b) {
345 if ($b['exclude']) {
346 if ($branches['exclude']) {
347 // conjunctive, so just add all branch names to be excluded
348 // !=a && !=b => !=a,!=b
349 $branches['names'] = array_merge($branches['names'], $b['names']);
350 } else {
351 // conjunctive, so only keep included names which are not excluded
352 // (==a||==c) && !=a,!=b => ==c
353 $branches['names'] = array_diff($branches['names'], $b['names']);
354 }
355 } else {
356 if ($branches['exclude']) {
357 // conjunctive, so only keep included names which are not excluded
358 // !=a,!=b && (==a||==c) => ==c
359 $branches['names'] = array_diff($b['names'], $branches['names']);
360 $branches['exclude'] = false;
361 } else {
362 // conjunctive, so only keep names that are included in both
363 // (==a||==b) && (==a||==c) => ==a
364 $branches['names'] = array_intersect($branches['names'], $b['names']);
365 }
366 }
367 }
368 }
369
370 $branches['names'] = array_unique($branches['names']);
371
372 if (\count($numericGroups) === 1) {
373 return array('numeric' => $numericGroups[0], 'branches' => $branches);
374 }
375
376 $borders = array();
377 foreach ($numericGroups as $group) {
378 foreach ($group as $interval) {
379 $borders[] = array('version' => $interval->getStart()->getVersion(), 'operator' => $interval->getStart()->getOperator(), 'side' => 'start');
380 $borders[] = array('version' => $interval->getEnd()->getVersion(), 'operator' => $interval->getEnd()->getOperator(), 'side' => 'end');
381 }
382 }
383
384 $opSortOrder = self::$opSortOrder;
385 usort($borders, function ($a, $b) use ($opSortOrder) {
386 $order = version_compare($a['version'], $b['version']);
387 if ($order === 0) {
388 return $opSortOrder[$a['operator']] - $opSortOrder[$b['operator']];
389 }
390
391 return $order;
392 });
393
394 $activeIntervals = 0;
395 $intervals = array();
396 $index = 0;
397 $activationThreshold = $constraint->isConjunctive() ? \count($numericGroups) : 1;
398 $start = null;
399 foreach ($borders as $border) {
400 if ($border['side'] === 'start') {
401 $activeIntervals++;
402 } else {
403 $activeIntervals--;
404 }
405 if (!$start && $activeIntervals >= $activationThreshold) {
406 $start = new Constraint($border['operator'], $border['version']);
407 } elseif ($start && $activeIntervals < $activationThreshold) {
408 // filter out invalid intervals like > x - <= x, or >= x - < x
409 if (
410 version_compare($start->getVersion(), $border['version'], '=')
411 && (
412 ($start->getOperator() === '>' && $border['operator'] === '<=')
413 || ($start->getOperator() === '>=' && $border['operator'] === '<')
414 )
415 ) {
416 unset($intervals[$index]);
417 } else {
418 $intervals[$index] = new Interval($start, new Constraint($border['operator'], $border['version']));
419 $index++;
420
421 if ($stopOnFirstValidInterval) {
422 break;
423 }
424 }
425
426 $start = null;
427 }
428 }
429
430 return array('numeric' => $intervals, 'branches' => $branches);
431 }
432
433 /**
434 * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}}
435 */
436 private static function generateSingleConstraintIntervals(Constraint $constraint)
437 {
438 $op = $constraint->getOperator();
439
440 // handle branch constraints first
441 if (strpos($constraint->getVersion(), 'dev-') === 0) {
442 $intervals = array();
443 $branches = array('names' => array(), 'exclude' => false);
444
445 // != dev-foo means any numeric version may match, we treat >/< like != they are not really defined for branches
446 if ($op === '!=') {
447 $intervals[] = new Interval(Interval::fromZero(), Interval::untilPositiveInfinity());
448 $branches = array('names' => array($constraint->getVersion()), 'exclude' => true);
449 } elseif ($op === '==') {
450 $branches['names'][] = $constraint->getVersion();
451 }
452
453 return array(
454 'numeric' => $intervals,
455 'branches' => $branches,
456 );
457 }
458
459 if ($op[0] === '>') { // > & >=
460 return array('numeric' => array(new Interval($constraint, Interval::untilPositiveInfinity())), 'branches' => Interval::noDev());
461 }
462 if ($op[0] === '<') { // < & <=
463 return array('numeric' => array(new Interval(Interval::fromZero(), $constraint)), 'branches' => Interval::noDev());
464 }
465 if ($op === '!=') {
466 // convert !=x to intervals of 0 - <x && >x - +inf + dev*
467 return array('numeric' => array(
468 new Interval(Interval::fromZero(), new Constraint('<', $constraint->getVersion())),
469 new Interval(new Constraint('>', $constraint->getVersion()), Interval::untilPositiveInfinity()),
470 ), 'branches' => Interval::anyDev());
471 }
472
473 // convert ==x to an interval of >=x - <=x
474 return array('numeric' => array(
475 new Interval(new Constraint('>=', $constraint->getVersion()), new Constraint('<=', $constraint->getVersion())),
476 ), 'branches' => Interval::noDev());
477 }
478 }
479