Completed
Push — add/changelog-tooling ( 86359e...64ad4d )
by
unknown
40:44 queued 31:04
created

Utils::loadAllChanges()   B

Complexity

Conditions 11
Paths 33

Size

Total Lines 40

Duplication

Lines 6
Ratio 15 %

Importance

Changes 0
Metric Value
cc 11
nc 33
nop 5
dl 6
loc 40
rs 7.3166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php // phpcs:ignore WordPress.Files.FileName
2
/**
3
 * Utilities for the changelogger tool.
4
 *
5
 * @package automattic/jetpack-changelogger
6
 */
7
8
// phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid, WordPress.NamingConventions.ValidVariableName
9
10
namespace Automattic\Jetpack\Changelogger;
11
12
use Automattic\Jetpack\Changelog\ChangeEntry;
13
use Symfony\Component\Console\Helper\DebugFormatterHelper;
14
use Symfony\Component\Console\Output\OutputInterface;
15
use Symfony\Component\Process\Process;
16
use function error_clear_last; // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.error_clear_lastFound
17
use function Wikimedia\quietCall;
18
19
/**
20
 * Utilities for the changelogger tool.
21
 */
22
class Utils {
23
24
	/**
25
	 * Calls `error_clear_last()` or emulates it.
26
	 */
27
	public static function error_clear_last() {
28
		if ( is_callable( 'error_clear_last' ) ) {
29
			// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.error_clear_lastFound
30
			error_clear_last();
31
		} else {
32
			// @codeCoverageIgnoreStart
33
			quietCall( 'trigger_error', '', E_USER_NOTICE );
34
			// @codeCoverageIgnoreEnd
35
		}
36
	}
37
38
	/**
39
	 * Helper to run a process.
40
	 *
41
	 * @param string[]             $command Command to execute.
42
	 * @param OutputInterface      $output OutputInterface to write debug output to.
43
	 * @param DebugFormatterHelper $formatter Formatter to use to format debug output.
44
	 * @param array                $options An associative array with the following optional keys. Defaults are null unless otherwise specified.
45
	 *                 - cwd: (string|null) The working directory or null to use the working dir of the current PHP process.
46
	 *                 - env: (array|null) The environment variables or null to use the same environment as the current PHP process.
47
	 *                 - input: (mixed|null) The input as stream resource, scalar or \Traversable, or null for no input.
48
	 *                 - timeout: (float|null) The timeout in seconds or null to disable. Default 60.
49
	 *                 - mustRun: (boolean) If set true, an exception will be thrown if the command fails. Default false.
50
	 * @return Process The process, which has already been run.
51
	 */
52
	public static function runCommand( array $command, OutputInterface $output, DebugFormatterHelper $formatter, array $options = array() ) {
53
		$options += array(
54
			'cwd'     => null,
55
			'env'     => null,
56
			'input'   => null,
57
			'timeout' => 60,
58
			'mustRun' => false,
59
		);
60
61
		$process = new Process( $command, $options['cwd'], $options['env'], $options['input'], $options['timeout'] );
62
		$output->writeln(
63
			$formatter->start( spl_object_hash( $process ), $process->getCommandLine() ),
64
			OutputInterface::VERBOSITY_DEBUG
65
		);
66
		$func = $options['mustRun'] ? 'mustRun' : 'run';
67
		$process->$func(
68
			function ( $type, $buffer ) use ( $output, $formatter, $process ) {
69
				$output->writeln(
70
					$formatter->progress( spl_object_hash( $process ), $buffer, Process::ERR === $type ),
71
					OutputInterface::VERBOSITY_DEBUG
72
				);
73
			}
74
		);
75
		return $process;
76
	}
77
78
	/**
79
	 * Load and parse a change file to an array.
80
	 *
81
	 * Header names are normalized. The entry is returned under the empty
82
	 * string key.
83
	 *
84
	 * @param string $filename File to load.
85
	 * @param mixed  $diagnostics Output variable, set to an array with diagnostic data.
86
	 *   - warnings: An array of warning messages and applicable lines.
87
	 *   - lines: An array mapping headers to line numbers.
88
	 * @return array
89
	 * @throws \RuntimeException On error.
90
	 */
91
	public static function loadChangeFile( $filename, &$diagnostics = null ) {
92
		$diagnostics = array(
93
			'warnings' => array(),
94
			'lines'    => array(),
95
		);
96
97
		if ( ! file_exists( $filename ) ) {
98
			$ex           = new \RuntimeException( 'File does not exist.' );
99
			$ex->fileLine = null;
0 ignored issues
show
Bug introduced by
The property fileLine does not seem to exist in RuntimeException.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
100
			throw $ex;
101
		}
102
103
		$fileinfo = new \SplFileInfo( $filename );
104
		if ( $fileinfo->getType() !== 'file' ) {
105
			$ex           = new \RuntimeException( "Expected a file, got {$fileinfo->getType()}." );
106
			$ex->fileLine = null;
107
			throw $ex;
108
		}
109
		if ( ! $fileinfo->isReadable() ) {
110
			$ex           = new \RuntimeException( 'File is not readable.' );
111
			$ex->fileLine = null;
112
			throw $ex;
113
		}
114
115
		self::error_clear_last();
116
		$contents = quietCall( 'file_get_contents', $filename );
117
		// @codeCoverageIgnoreStart
118
		if ( false === $contents ) {
119
			$err          = error_get_last();
120
			$ex           = new \RuntimeException( "Failed to read file: {$err['message']}" );
121
			$ex->fileLine = null;
122
			throw $ex;
123
		}
124
		// @codeCoverageIgnoreEnd
125
126
		$ret  = array();
127
		$line = 1;
128
		while ( preg_match( '/^([A-Z][a-zA-Z0-9-]*):((?:.|\n[ \t])*)(?:\n|$)/', $contents, $m ) ) {
129
			if ( isset( $diagnostics['lines'][ $m[1] ] ) ) {
130
				$diagnostics['warnings'][] = array(
131
					"Duplicate header \"{$m[1]}\", previously seen on line {$diagnostics['lines'][ $m[1] ]}.",
132
					$line,
133
				);
134
			} else {
135
				$diagnostics['lines'][ $m[1] ] = $line;
136
				$ret[ $m[1] ]                  = trim( preg_replace( '/(\n[ \t]+)+/', ' ', $m[2] ) );
137
			}
138
			$line    += substr_count( $m[0], "\n" );
139
			$contents = (string) substr( $contents, strlen( $m[0] ) );
140
		}
141
142
		if ( '' !== $contents && "\n" !== $contents[0] ) {
143
			$ex           = new \RuntimeException( 'Invalid header.' );
144
			$ex->fileLine = $line;
145
			throw $ex;
146
		}
147
		$diagnostics['lines'][''] = $line + strspn( $contents, "\n" );
148
		$ret['']                  = trim( $contents );
149
150
		return $ret;
151
	}
152
153
	/**
154
	 * Load the changes files into an array of ChangeEntries.
155
	 *
156
	 * @param string          $dir Changes directory.
157
	 * @param array           $subheadings Mapping from type codes to subheadings.
158
	 * @param FormatterPlugin $formatter Formatter plugin to use.
159
	 * @param OutputInterface $output OutputInterface to write diagnostics too.
160
	 * @param mixed           $files Output parameter, a list of files successfully processed is written to this variable.
161
	 * @return ChangeEntry[]
162
	 */
163
	public static function loadAllChanges( $dir, array $subheadings, FormatterPlugin $formatter, OutputInterface $output, &$files = null ) {
164
		$files = array();
165
		$ret   = array();
166
167
		$allFiles = array();
168 View Code Duplication
		foreach ( new \DirectoryIterator( $dir ) as $file ) {
169
			$name = $file->getBasename();
170
			if ( '.' !== $name[0] ) {
171
				$allFiles[ $name ] = $file->getPathname();
172
			}
173
		}
174
		asort( $allFiles );
175
		foreach ( $allFiles as $name => $path ) {
176
			$diagnostics = null;
177
			try {
178
				$data = self::loadChangeFile( $path, $diagnostics );
179
			} catch ( \RuntimeException $ex ) {
180
				$output->writeln( "<error>$name: {$ex->getMessage()}</>" );
181
				continue;
182
			}
183
			foreach ( $diagnostics['warnings'] as list( $msg, $line ) ) {
184
				$line = $line ? ":$line" : '';
185
				$output->writeln( "<warning>$name$line: $msg</>" );
186
			}
187
			try {
188
				$ret[]   = $formatter->newChangeEntry(
189
					array(
190
						'significance' => isset( $data['Significance'] ) ? $data['Significance'] : null,
191
						'subheading'   => isset( $data['Type'] ) ? ( isset( $subheadings[ $data['Type'] ] ) ? $subheadings[ $data['Type'] ] : ucfirst( $data['Type'] ) ) : null,
192
						'content'      => $data[''],
193
					)
194
				);
195
				$files[] = $path;
196
			} catch ( \InvalidArgumentException $ex ) {
197
				$output->writeln( "<error>$name: {$ex->getMessage()}</>" );
198
			}
199
		}
200
201
		return $ret;
202
	}
203
204
}
205
206