Intervals.php 20 KB

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