Skip to content

Commit fd07357

Browse files
committed
Improve searching min required version
1 parent 0a97b84 commit fd07357

4 files changed

Lines changed: 157 additions & 9 deletions

File tree

src/Composer/Package.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace RoadRunner\VersionChecker\Composer;
66

77
use Composer\InstalledVersions;
8+
use Composer\Semver\VersionParser;
89

910
final class Package implements PackageInterface
1011
{
@@ -21,12 +22,48 @@ public function getRequiredVersions(string $packageName): array
2122
/** @var array{require?: array<non-empty-string, non-empty-string>} $composerJson */
2223
$composerJson = \json_decode(\file_get_contents($path . '/composer.json'), true);
2324

24-
if (isset($composerJson['require'][$packageName])) {
25-
$versions[] = $composerJson['require'][$packageName];
25+
if (
26+
isset($composerJson['require'][$packageName]) &&
27+
$this->isSupportedVersion($composerJson['require'][$packageName])
28+
) {
29+
$versions[] = $this->getMinVersion($composerJson['require'][$packageName]);
2630
}
2731
}
2832
}
2933

3034
return $versions;
3135
}
36+
37+
/**
38+
* @param non-empty-string $version
39+
*/
40+
private function isSupportedVersion(string $version): bool
41+
{
42+
$parser = new VersionParser();
43+
44+
try {
45+
$parser->parseConstraints($version);
46+
} catch (\Throwable) {
47+
return false;
48+
}
49+
50+
return !\str_starts_with($version, 'dev-');
51+
}
52+
53+
/**
54+
* @param non-empty-string $version
55+
*
56+
* @return non-empty-string
57+
*/
58+
private function getMinVersion(string $version): string
59+
{
60+
$parser = new VersionParser();
61+
62+
$constraint = $parser->parseConstraints($version);
63+
64+
/** @var non-empty-string $min */
65+
$min = $constraint->getLowerBound()->getVersion();
66+
67+
return $min;
68+
}
3269
}

src/Version/Required.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
namespace RoadRunner\VersionChecker\Version;
66

77
use Composer\Semver\Comparator as SemverComparator;
8-
use Composer\Semver\VersionParser;
98
use RoadRunner\VersionChecker\Composer\Package;
109
use RoadRunner\VersionChecker\Composer\PackageInterface;
1110

1211
final class Required implements RequiredInterface
1312
{
1413
private const ROADRUNNER_PACKAGE = 'spiral/roadrunner';
1514

15+
/**
16+
* @var non-empty-string|null
17+
*/
18+
private static ?string $cachedVersion = null;
19+
1620
public function __construct(
1721
private readonly PackageInterface $package = new Package()
1822
) {
@@ -23,16 +27,15 @@ public function __construct(
2327
*/
2428
public function getRequiredVersion(): ?string
2529
{
26-
$parser = new VersionParser();
30+
if (self::$cachedVersion !== null) {
31+
return self::$cachedVersion;
32+
}
2733

28-
$version = null;
2934
foreach ($this->package->getRequiredVersions(self::ROADRUNNER_PACKAGE) as $packageVersion) {
30-
/** @var non-empty-string $packageVersion */
31-
$packageVersion = $parser->normalize($packageVersion);
32-
$version = $this->getMinimumVersion($packageVersion, $version);
35+
self::$cachedVersion = $this->getMinimumVersion($packageVersion, self::$cachedVersion);
3336
}
3437

35-
return $version;
38+
return self::$cachedVersion;
3639
}
3740

3841
/**
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RoadRunner\VersionChecker\Tests\Unit\Composer;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use RoadRunner\VersionChecker\Composer\Package;
9+
10+
final class PackageTest extends TestCase
11+
{
12+
/**
13+
* @dataProvider isSupportedVersionDataProvider
14+
*/
15+
public function testIsSupportedVersion(string $version, bool $expected): void
16+
{
17+
$package = new Package();
18+
$ref = new \ReflectionMethod($package, 'isSupportedVersion');
19+
20+
$this->assertSame($expected, $ref->invoke($package, $version));
21+
}
22+
23+
/**
24+
* @dataProvider getMinVersionDataProvider
25+
*/
26+
public function testGetMinVersion(string $version, string $expected): void
27+
{
28+
$package = new Package();
29+
$ref = new \ReflectionMethod($package, 'getMinVersion');
30+
31+
$this->assertSame($expected, $ref->invoke($package, $version));
32+
}
33+
34+
public static function isSupportedVersionDataProvider(): \Traversable
35+
{
36+
yield ['1.0', true];
37+
yield ['1.0.0', true];
38+
yield ['^1.0', true];
39+
yield ['>=1.0', true];
40+
yield ['>1.0', true];
41+
yield ['1.0.*', true];
42+
yield ['^1.0 | ^2.0', true];
43+
yield ['^1.0 || ^2.0', true];
44+
yield ['1.0 - 2.0', true];
45+
yield ['dev-master', false];
46+
yield ['dev-feature/some', false];
47+
yield ['<2.0', true];
48+
yield ['<=2.0', true];
49+
}
50+
51+
public static function getMinVersionDataProvider(): \Traversable
52+
{
53+
yield ['1.0', '1.0.0.0'];
54+
yield ['1.0.0', '1.0.0.0'];
55+
yield ['^1.0', '1.0.0.0-dev'];
56+
yield ['>=1.0', '1.0.0.0-dev'];
57+
yield ['>1.0', '1.0.0.0'];
58+
yield ['1.0.*', '1.0.0.0-dev'];
59+
yield ['1.0.1', '1.0.1.0'];
60+
yield ['1.1.*', '1.1.0.0-dev'];
61+
yield ['1.1.1', '1.1.1.0'];
62+
yield ['^1.0 | ^2.0', '1.0.0.0-dev'];
63+
yield ['^1.0 || ^2.0', '1.0.0.0-dev'];
64+
yield ['1.0 - 2.0', '1.0.0.0-dev'];
65+
yield ['<2.0', '0.0.0.0-dev'];
66+
yield ['<=2.0', '0.0.0.0-dev'];
67+
}
68+
}

tests/src/Unit/Version/RequiredTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@
55
namespace RoadRunner\VersionChecker\Tests\Unit\Version;
66

77
use PHPUnit\Framework\TestCase;
8+
use RoadRunner\VersionChecker\Composer\PackageInterface;
89
use RoadRunner\VersionChecker\Version\Required;
910

1011
final class RequiredTest extends TestCase
1112
{
13+
protected function tearDown(): void
14+
{
15+
// clean the cache
16+
(new \ReflectionProperty(Required::class, 'cachedVersion'))->setValue(null);
17+
}
18+
1219
/**
1320
* @dataProvider versionsDataProvider
1421
*/
@@ -20,6 +27,36 @@ public function testGetMinimumVersion(string $version, ?string $previous, string
2027
$this->assertSame($expected, $ref->invoke($required, $version, $previous));
2128
}
2229

30+
public function testGetRequiredVersion(): void
31+
{
32+
$package = $this->createMock(PackageInterface::class);
33+
$package
34+
->expects($this->once())
35+
->method('getRequiredVersions')
36+
->with('spiral/roadrunner')
37+
->willReturn(['2.0', '1.0', '2.0.0.0-dev', '2.0.0-alpha']);
38+
39+
$required = new Required($package);
40+
41+
$this->assertSame('1.0', $required->getRequiredVersion());
42+
}
43+
44+
public function testGetCachedVersion(): void
45+
{
46+
$package = $this->createMock(PackageInterface::class);
47+
$package
48+
// $this->once() is important for this test!
49+
->expects($this->once())
50+
->method('getRequiredVersions')
51+
->with('spiral/roadrunner')
52+
->willReturn(['1.0']);
53+
54+
$required = new Required($package);
55+
56+
$this->assertSame('1.0', $required->getRequiredVersion());
57+
$this->assertSame('1.0', $required->getRequiredVersion());
58+
}
59+
2360
public static function versionsDataProvider(): \Traversable
2461
{
2562
// Test case with $previous === null
@@ -33,5 +70,8 @@ public static function versionsDataProvider(): \Traversable
3370
yield ['2.0.0', '1.0.0', '1.0.0'];
3471
yield ['1.0.0', '1.0.0-alpha', '1.0.0-alpha'];
3572
yield ['1.0.0', '1.0.0', '1.0.0'];
73+
74+
yield ['1.0.0.0', '1.1.0', '1.0.0.0'];
75+
yield ['1.0.0.0-dev', '1.0.0.0', '1.0.0.0-dev'];
3676
}
3777
}

0 commit comments

Comments
 (0)