Completed
Push — add/changelog-tooling ( 7f5585...86359e )
by
unknown
58:45 queued 48:46
created

ValidateCommand::execute()   B

Complexity

Conditions 8
Paths 24

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 24
nop 2
dl 0
loc 28
rs 8.4444
c 0
b 0
f 0
1
<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase
2
/**
3
 * "Validate" command for the changelogger tool CLI.
4
 *
5
 * @package automattic/jetpack-changelogger
6
 */
7
8
// phpcs:disable WordPress.NamingConventions.ValidVariableName
9
10
namespace Automattic\Jetpack\Changelogger;
11
12
use Symfony\Component\Console\Command\Command;
13
use Symfony\Component\Console\Input\InputArgument;
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Input\InputOption;
16
use Symfony\Component\Console\Output\OutputInterface;
17
18
/**
19
 * "Validate" command for the changelogger tool CLI.
20
 */
21
class ValidateCommand extends Command {
22
23
	/**
24
	 * The default command name
25
	 *
26
	 * @var string|null
27
	 */
28
	protected static $defaultName = 'validate';
29
30
	/**
31
	 * The InputInterface to use.
32
	 *
33
	 * @var InputInterface|null
34
	 */
35
	private $input;
36
37
	/**
38
	 * The OutputInterface to use.
39
	 *
40
	 * @var OutputInterface|null
41
	 */
42
	private $output;
43
44
	/**
45
	 * Counts of errors and warnings output.
46
	 *
47
	 * @var int[]
48
	 */
49
	private $counts;
50
51
	/**
52
	 * Configures the command.
53
	 */
54
	protected function configure() {
55
		$this->setDescription( 'Validates changelog entry files' )
56
			->addOption( 'gh-action', null, InputOption::VALUE_NONE, 'Output validation issues using GitHub Action command syntax.' )
57
			->addOption( 'no-strict', null, InputOption::VALUE_NONE, 'Do not exit with a failure code if only warnings are found.' )
58
			->addArgument( 'files', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Files to check. By default, all change files in the changelog directory are checked.' )
59
			->setHelp(
60
				<<<EOF
61
The <info>validate</info> command validates change files.
62
EOF
63
			);
64
	}
65
66
	/**
67
	 * Output an error or warning.
68
	 *
69
	 * @param string   $type 'error' or 'warning'.
70
	 * @param string   $file Filename with the error/warning.
71
	 * @param int|null $line Line number of the error/warning.
72
	 * @param string   $msg Error message.
73
	 */
74
	private function msg( $type, $file, $line, $msg ) {
75
		if ( $this->input->getOption( 'gh-action' ) ) {
76
			$prefix = "::$type file=$file";
77
			if ( null !== $line ) {
78
				$prefix .= ",line=$line";
79
			}
80
			$prefix .= '::';
81
			$postfix = '';
82
		} else {
83
			$prefix = "<$type>$file";
84
			if ( null !== $line ) {
85
				$prefix .= ":$line";
86
			}
87
			$prefix .= ': ';
88
			$postfix = '</>';
89
		}
90
		$this->output->writeln( $prefix . $msg . $postfix );
91
		$this->counts[ $type ]++;
92
	}
93
94
	/**
95
	 * Validate a file.
96
	 *
97
	 * @param string $filename Filename.
98
	 */
99
	public function validateFile( $filename ) {
100
		try {
101
			$diagnostics = null; // Make phpcs happy.
102
			$data        = Utils::loadChangeFile( $filename, $diagnostics );
103
		} catch ( \RuntimeException $ex ) {
104
			$this->msg( 'error', $filename, $ex->fileLine, $ex->getMessage() );
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...
105
			return false;
106
		}
107
108
		$messages = array();
109
110
		foreach ( $diagnostics['warnings'] as list( $msg, $line ) ) {
111
			$messages[] = array( 'warning', $msg, $line );
112
		}
113
114
		foreach ( $diagnostics['lines'] as $header => $line ) {
115
			if ( ! in_array( $header, array( 'Significance', 'Type', 'Comment', '' ), true ) ) {
116
				$messages[] = array( 'warning', "Unrecognized header \"$header\".", $line );
117
			}
118
		}
119
120
		if ( ! isset( $data['Significance'] ) ) {
121
			$messages[] = array( 'error', 'File does not contain a Significance header.', null );
122
		} elseif ( ! in_array( $data['Significance'], array( 'patch', 'minor', 'major' ), true ) ) {
123
			$messages[] = array( 'error', 'Significance must be "patch", "minor", or "major".', $diagnostics['lines']['Significance'] );
124
		} elseif ( 'patch' !== $data['Significance'] && '' === $data[''] ) {
125
			$messages[] = array( 'error', 'Changelog entry may only be empty when Significance is "patch".', $diagnostics['lines'][''] );
126
		}
127
128
		$types = Config::types();
129
		if ( $types ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $types of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
130
			if ( ! isset( $data['Type'] ) ) {
131
				$messages[] = array( 'error', 'File does not contain a Type header.', null );
132
			} elseif ( ! isset( $types[ $data['Type'] ] ) ) {
133
				$list = array_map(
134
					function ( $v ) {
135
						return "\"$v\"";
136
					},
137
					array_keys( $types )
138
				);
139 View Code Duplication
				if ( count( $list ) > 1 ) {
140
					$list[ count( $list ) - 1 ] = 'or ' . $list[ count( $list ) - 1 ];
141
				}
142
				$messages[] = array( 'error', 'Type must be ' . implode( count( $list ) > 2 ? ', ' : ' ', $list ) . '.', $diagnostics['lines']['Type'] );
143
			}
144
		}
145
146
		usort(
147
			$messages,
148
			function ( $a, $b ) {
149
				// @codeCoverageIgnoreStart
150 View Code Duplication
				if ( $a[2] !== $b[2] ) {
151
					return $a[2] - $b[2];
152
				}
153 View Code Duplication
				if ( $a[0] !== $b[0] ) {
154
					return strcmp( $a[0], $b[0] );
155
				}
156
				return strcmp( $a[1], $b[1] );
157
				// @codeCoverageIgnoreEnd
158
			}
159
		);
160
		foreach ( $messages as list( $type, $msg, $line ) ) {
161
			$this->msg( $type, $filename, $line, $msg );
162
		}
163
	}
164
165
	/**
166
	 * Executes the command.
167
	 *
168
	 * @param InputInterface  $input InputInterface.
169
	 * @param OutputInterface $output OutputInterface.
170
	 * @return int 0 if everything went fine, or an exit code.
171
	 */
172
	protected function execute( InputInterface $input, OutputInterface $output ) {
173
		$this->input  = $input;
174
		$this->output = $output;
175
		$this->counts = array(
0 ignored issues
show
Documentation Bug introduced by
It seems like array('error' => 0, 'warning' => 0) of type array<string,integer,{"e...","warning":"integer"}> is incompatible with the declared type array<integer,integer> of property $counts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
176
			'error'   => 0,
177
			'warning' => 0,
178
		);
179
180
		$files = $input->getArgument( 'files' );
181
		if ( ! $files ) {
182
			$files = array();
183
			foreach ( new \DirectoryIterator( Config::changesDir() ) as $file ) {
184
				$name = $file->getBasename();
185
				if ( '.' !== $name[0] ) {
186
					$files[] = $file->getPathname();
187
				}
188
			}
189
			sort( $files );
190
		}
191
192
		foreach ( $files as $filename ) {
193
			$output->writeln( "Checking $filename...", OutputInterface::VERBOSITY_VERBOSE );
194
			$this->validateFile( $filename );
195
		}
196
197
		$output->writeln( sprintf( 'Found %d error(s) and %d warning(s)', $this->counts['error'], $this->counts['warning'] ), OutputInterface::VERBOSITY_VERBOSE );
198
		return $this->counts['error'] || $this->counts['warning'] && ! $input->getOption( 'no-strict' ) ? 1 : 0;
199
	}
200
}
201