Completed
Push — update/use-identity-crisis-pac... ( d14b6d...6e141b )
by
unknown
128:58 queued 119:18
created

SemverVersioning::validateExtra()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 24

Duplication

Lines 24
Ratio 100 %

Importance

Changes 0
Metric Value
cc 5
nc 7
nop 1
dl 24
loc 24
rs 9.2248
c 0
b 0
f 0
1
<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase
2
/**
3
 * Semver versioning plugin.
4
 *
5
 * @package automattic/jetpack-changelogger
6
 */
7
8
namespace Automattic\Jetpack\Changelogger\Plugins;
9
10
use Automattic\Jetpack\Changelogger\PluginTrait;
11
use Automattic\Jetpack\Changelogger\VersioningPlugin;
12
use InvalidArgumentException;
13
14
/**
15
 * Semver versioning plugin.
16
 */
17
class SemverVersioning implements VersioningPlugin {
18
	use PluginTrait;
19
20
	/**
21
	 * Parse a semver version.
22
	 *
23
	 * @param string $version Version.
24
	 * @return array With components:
25
	 *  - major: (int) Major version.
26
	 *  - minor: (int) Minor version.
27
	 *  - patch: (int) Patch version.
28
	 *  - prerelease: (string|null) Pre-release string.
29
	 *  - buildinfo: (string|null) Build metadata string.
30
	 * @throws InvalidArgumentException If the version number is not in a recognized format.
31
	 */
32
	public function parseVersion( $version ) {
33
		// This is slightly looser than the official version from semver.org, in that leading zeros are allowed.
34
		if ( ! preg_match( '/^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<prerelease>(?:[0-9a-zA-Z-]+)(?:\.(?:[0-9a-zA-Z-]+))*))?(?:\+(?P<buildinfo>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/', $version, $m ) ) {
35
			throw new InvalidArgumentException( "Version number \"$version\" is not in a recognized format." );
36
		}
37
		return array(
38
			'major'      => (int) $m['major'],
39
			'minor'      => (int) $m['minor'],
40
			'patch'      => (int) $m['patch'],
41
			'prerelease' => isset( $m['prerelease'] ) && '' !== $m['prerelease'] ? $m['prerelease'] : null,
42
			'buildinfo'  => isset( $m['buildinfo'] ) && '' !== $m['buildinfo'] ? $m['buildinfo'] : null,
43
		);
44
	}
45
46
	/**
47
	 * Check and normalize a version number.
48
	 *
49
	 * @param string|array $version Version string, or array as from `parseVersion()`.
50
	 * @return string Normalized version.
51
	 * @throws InvalidArgumentException If the version number is not in a recognized format.
52
	 */
53
	public function normalizeVersion( $version ) {
54 View Code Duplication
		if ( is_array( $version ) ) {
55
			$info = $version + array(
56
				'prerelease' => null,
57
				'buildinfo'  => null,
58
			);
59
			$test = $this->parseVersion( '0.0.0' );
60
			if ( array_intersect_key( $test, $info ) !== $test ) {
61
				throw new InvalidArgumentException( 'Version array is not in a recognized format.' );
62
			}
63
		} else {
64
			$info = $this->parseVersion( $version );
65
		}
66
67
		$ret = sprintf( '%d.%d.%d', $info['major'], $info['minor'], $info['patch'] );
68
		if ( null !== $info['prerelease'] ) {
69
			$sep = '-';
70
			foreach ( explode( '.', $info['prerelease'] ) as $part ) {
71
				if ( ctype_digit( $part ) ) {
72
					$part = (int) $part;
73
				}
74
				$ret .= $sep . $part;
75
				$sep  = '.';
76
			}
77
		}
78
		if ( null !== $info['buildinfo'] ) {
79
			$ret .= '+' . $info['buildinfo'];
80
		}
81
		return $ret;
82
	}
83
84
	/**
85
	 * Validate an `$extra` array.
86
	 *
87
	 * @param array $extra Extra components for the version. See `nextVersion()`.
88
	 * @return array
89
	 * @throws InvalidArgumentException If the `$extra` data is invalid.
90
	 */
91 View Code Duplication
	private function validateExtra( array $extra ) {
92
		$info = array();
93
94
		if ( isset( $extra['prerelease'] ) ) {
95
			try {
96
				$info['prerelease'] = $this->parseVersion( '0.0.0-' . $extra['prerelease'] )['prerelease'];
97
			} catch ( InvalidArgumentException $ex ) {
98
				throw new InvalidArgumentException( 'Invalid prerelease data' );
99
			}
100
		} else {
101
			$info['prerelease'] = null;
102
		}
103
		if ( isset( $extra['buildinfo'] ) ) {
104
			try {
105
				$info['buildinfo'] = $this->parseVersion( '0.0.0+' . $extra['buildinfo'] )['buildinfo'];
106
			} catch ( InvalidArgumentException $ex ) {
107
				throw new InvalidArgumentException( 'Invalid buildinfo data' );
108
			}
109
		} else {
110
			$info['buildinfo'] = null;
111
		}
112
113
		return $info;
114
	}
115
116
	/**
117
	 * Determine the next version given a current version and a set of changes.
118
	 *
119
	 * @param string        $version Current version.
120
	 * @param ChangeEntry[] $changes Changes.
121
	 * @param array         $extra Extra components for the version.
122
	 *  - prerelease: (string|null) Prerelease version, e.g. "dev", "alpha", or "beta", if any. See semver docs for accepted values.
123
	 *  - buildinfo: (string|null) Build info, if any. See semver docs for accepted values.
124
	 * @return string
125
	 * @throws InvalidArgumentException If the version number is not in a recognized format, or other arguments are invalid.
126
	 */
127
	public function nextVersion( $version, array $changes, array $extra = array() ) {
128
		$info = array_merge(
129
			$this->parseVersion( $version ),
130
			$this->validateExtra( $extra )
131
		);
132
133
		$significances = array();
134
		foreach ( $changes as $change ) {
135
			$significances[ (string) $change->getSignificance() ] = true;
136
		}
137
		if ( isset( $significances['major'] ) ) {
138
			$info['patch'] = 0;
139
			if ( 0 === (int) $info['major'] ) {
140
				if ( is_callable( array( $this->output, 'getErrorOutput' ) ) ) {
141
					$out = $this->output->getErrorOutput();
142
					$out->writeln( '<warning>Semver does not automatically move version 0.y.z to 1.0.0.</>' );
143
					$out->writeln( '<warning>You will have to do that manually when you\'re ready for the first release.</>' );
144
				}
145
				$info['minor']++;
146
			} else {
147
				$info['minor'] = 0;
148
				$info['major']++;
149
			}
150
		} elseif ( isset( $significances['minor'] ) ) {
151
			$info['patch'] = 0;
152
			$info['minor']++;
153
		} else {
154
			$info['patch']++;
155
		}
156
157
		return $this->normalizeVersion( $info );
158
	}
159
160
	/**
161
	 * Compare two version numbers.
162
	 *
163
	 * @param string $a First version.
164
	 * @param string $b Second version.
165
	 * @return int Less than, equal to, or greater than 0 depending on whether `$a` is less than, equal to, or greater than `$b`.
166
	 * @throws InvalidArgumentException If the version numbers are not in a recognized format.
167
	 */
168
	public function compareVersions( $a, $b ) {
169
		$aa = $this->parseVersion( $a );
170
		$bb = $this->parseVersion( $b );
171
		if ( $aa['major'] !== $bb['major'] ) {
172
			return $aa['major'] - $bb['major'];
173
		}
174
		if ( $aa['minor'] !== $bb['minor'] ) {
175
			return $aa['minor'] - $bb['minor'];
176
		}
177 View Code Duplication
		if ( $aa['patch'] !== $bb['patch'] ) {
178
			return $aa['patch'] - $bb['patch'];
179
		}
180
181
		if ( null === $aa['prerelease'] ) {
182
			return null === $bb['prerelease'] ? 0 : 1;
183
		}
184
		if ( null === $bb['prerelease'] ) {
185
			return -1;
186
		}
187
188
		$aaa = explode( '.', $aa['prerelease'] );
189
		$bbb = explode( '.', $bb['prerelease'] );
190
		$al  = count( $aaa );
191
		$bl  = count( $bbb );
192
		for ( $i = 0; $i < $al && $i < $bl; $i++ ) {
193
			$a = $aaa[ $i ];
194
			$b = $bbb[ $i ];
195
			if ( ctype_digit( $a ) ) {
196
				if ( ctype_digit( $b ) ) {
197
					if ( (int) $a !== (int) $b ) {
198
						return $a - $b;
199
					}
200
				} else {
201
					return -1;
202
				}
203
			} elseif ( ctype_digit( $b ) ) {
204
				return 1;
205
			} else {
206
				$tmp = strcmp( $a, $b );
207
				if ( 0 !== $tmp ) {
208
					return $tmp;
209
				}
210
			}
211
		}
212
		return $al - $bl;
213
	}
214
215
	/**
216
	 * Return a valid "first" version number.
217
	 *
218
	 * @param array $extra Extra components for the version, as for `nextVersion()`.
219
	 * @return string
220
	 */
221
	public function firstVersion( array $extra = array() ) {
222
		return $this->normalizeVersion(
223
			array(
224
				'major' => 0,
225
				'minor' => 1,
226
				'patch' => 0,
227
			) + $this->validateExtra( $extra )
228
		);
229
	}
230
231
}
232