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
|
|
|
* Base directory regex. |
53
|
|
|
* |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
private $basedirRegex; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Configures the command. |
60
|
|
|
*/ |
61
|
|
|
protected function configure() { |
62
|
|
|
$this->setDescription( 'Validates changelog entry files' ) |
63
|
|
|
->addOption( 'gh-action', null, InputOption::VALUE_NONE, 'Output validation issues using GitHub Action command syntax.' ) |
64
|
|
|
->addOption( 'basedir', null, InputOption::VALUE_REQUIRED, 'Output file paths in this directory relative to it.' ) |
65
|
|
|
->addOption( 'no-strict', null, InputOption::VALUE_NONE, 'Do not exit with a failure code if only warnings are found.' ) |
66
|
|
|
->addArgument( 'files', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Files to check. By default, all change files in the changelog directory are checked.' ) |
67
|
|
|
->setHelp( |
68
|
|
|
<<<EOF |
69
|
|
|
The <info>validate</info> command validates change files. |
70
|
|
|
EOF |
71
|
|
|
); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Output an error or warning. |
76
|
|
|
* |
77
|
|
|
* @param string $type 'error' or 'warning'. |
78
|
|
|
* @param string $file Filename with the error/warning. |
79
|
|
|
* @param int|null $line Line number of the error/warning. |
80
|
|
|
* @param string $msg Error message. |
81
|
|
|
*/ |
82
|
|
|
private function msg( $type, $file, $line, $msg ) { |
83
|
|
|
$file = preg_replace( $this->basedirRegex, '', $file ); |
84
|
|
|
if ( $this->input->getOption( 'gh-action' ) ) { |
85
|
|
|
$prefix = "::$type file=$file"; |
86
|
|
|
if ( null !== $line ) { |
87
|
|
|
$prefix .= ",line=$line"; |
88
|
|
|
} |
89
|
|
|
$prefix .= '::'; |
90
|
|
|
$postfix = ''; |
91
|
|
|
} else { |
92
|
|
|
$prefix = "<$type>$file"; |
93
|
|
|
if ( null !== $line ) { |
94
|
|
|
$prefix .= ":$line"; |
95
|
|
|
} |
96
|
|
|
$prefix .= ': '; |
97
|
|
|
$postfix = '</>'; |
98
|
|
|
} |
99
|
|
|
$this->output->writeln( $prefix . $msg . $postfix ); |
100
|
|
|
$this->counts[ $type ]++; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Validate a file. |
105
|
|
|
* |
106
|
|
|
* @param string $filename Filename. |
107
|
|
|
*/ |
108
|
|
|
public function validateFile( $filename ) { |
109
|
|
|
try { |
110
|
|
|
$diagnostics = null; // Make phpcs happy. |
111
|
|
|
$data = Utils::loadChangeFile( $filename, $diagnostics ); |
112
|
|
|
} catch ( \RuntimeException $ex ) { |
113
|
|
|
$this->msg( 'error', $filename, $ex->fileLine, $ex->getMessage() ); |
|
|
|
|
114
|
|
|
return false; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
$messages = array(); |
118
|
|
|
|
119
|
|
|
foreach ( $diagnostics['warnings'] as list( $msg, $line ) ) { |
120
|
|
|
$messages[] = array( 'warning', $msg, $line ); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
foreach ( $diagnostics['lines'] as $header => $line ) { |
124
|
|
|
if ( ! in_array( $header, array( 'Significance', 'Type', 'Comment', '' ), true ) ) { |
125
|
|
|
$messages[] = array( 'warning', "Unrecognized header \"$header\".", $line ); |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
if ( ! isset( $data['Significance'] ) ) { |
130
|
|
|
$messages[] = array( 'error', 'File does not contain a Significance header.', null ); |
131
|
|
|
} elseif ( ! in_array( $data['Significance'], array( 'patch', 'minor', 'major' ), true ) ) { |
132
|
|
|
$messages[] = array( 'error', 'Significance must be "patch", "minor", or "major".', $diagnostics['lines']['Significance'] ); |
133
|
|
|
} elseif ( 'patch' !== $data['Significance'] && '' === $data[''] ) { |
134
|
|
|
$messages[] = array( 'error', 'Changelog entry may only be empty when Significance is "patch".', $diagnostics['lines'][''] ); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$types = Config::types(); |
138
|
|
|
if ( $types ) { |
|
|
|
|
139
|
|
|
if ( ! isset( $data['Type'] ) ) { |
140
|
|
|
$messages[] = array( 'error', 'File does not contain a Type header.', null ); |
141
|
|
|
} elseif ( ! isset( $types[ $data['Type'] ] ) ) { |
142
|
|
|
$list = array_map( |
143
|
|
|
function ( $v ) { |
144
|
|
|
return "\"$v\""; |
145
|
|
|
}, |
146
|
|
|
array_keys( $types ) |
147
|
|
|
); |
148
|
|
View Code Duplication |
if ( count( $list ) > 1 ) { |
149
|
|
|
$list[ count( $list ) - 1 ] = 'or ' . $list[ count( $list ) - 1 ]; |
150
|
|
|
} |
151
|
|
|
$messages[] = array( 'error', 'Type must be ' . implode( count( $list ) > 2 ? ', ' : ' ', $list ) . '.', $diagnostics['lines']['Type'] ); |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
usort( |
156
|
|
|
$messages, |
157
|
|
|
function ( $a, $b ) { |
158
|
|
|
// @codeCoverageIgnoreStart |
159
|
|
View Code Duplication |
if ( $a[2] !== $b[2] ) { |
160
|
|
|
return $a[2] - $b[2]; |
161
|
|
|
} |
162
|
|
View Code Duplication |
if ( $a[0] !== $b[0] ) { |
163
|
|
|
return strcmp( $a[0], $b[0] ); |
164
|
|
|
} |
165
|
|
|
return strcmp( $a[1], $b[1] ); |
166
|
|
|
// @codeCoverageIgnoreEnd |
167
|
|
|
} |
168
|
|
|
); |
169
|
|
|
foreach ( $messages as list( $type, $msg, $line ) ) { |
170
|
|
|
$this->msg( $type, $filename, $line, $msg ); |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Executes the command. |
176
|
|
|
* |
177
|
|
|
* @param InputInterface $input InputInterface. |
178
|
|
|
* @param OutputInterface $output OutputInterface. |
179
|
|
|
* @return int 0 if everything went fine, or an exit code. |
180
|
|
|
*/ |
181
|
|
|
protected function execute( InputInterface $input, OutputInterface $output ) { |
182
|
|
|
$this->input = $input; |
183
|
|
|
$this->output = $output; |
184
|
|
|
$this->counts = array( |
|
|
|
|
185
|
|
|
'error' => 0, |
186
|
|
|
'warning' => 0, |
187
|
|
|
); |
188
|
|
|
|
189
|
|
|
if ( $input->getOption( 'basedir' ) ) { |
190
|
|
|
$basedir = rtrim( $input->getOption( 'basedir' ), '/' ); |
191
|
|
|
$basedir = rtrim( $basedir, DIRECTORY_SEPARATOR ); |
192
|
|
|
$this->basedirRegex = '#^' . preg_quote( $basedir, '#' ) . '[/' . preg_quote( DIRECTORY_SEPARATOR, '#' ) . ']#'; |
193
|
|
|
} else { |
194
|
|
|
$this->basedirRegex = '/(?!)/'; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
$files = $input->getArgument( 'files' ); |
198
|
|
|
if ( ! $files ) { |
199
|
|
|
$files = array(); |
200
|
|
View Code Duplication |
foreach ( new \DirectoryIterator( Config::changesDir() ) as $file ) { |
201
|
|
|
$name = $file->getBasename(); |
202
|
|
|
if ( '.' !== $name[0] ) { |
203
|
|
|
$files[] = $file->getPathname(); |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
sort( $files ); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
foreach ( $files as $filename ) { |
210
|
|
|
$file = preg_replace( $this->basedirRegex, '', $filename ); |
211
|
|
|
$output->writeln( "Checking $file...", OutputInterface::VERBOSITY_VERBOSE ); |
212
|
|
|
$this->validateFile( $filename ); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
$output->writeln( sprintf( 'Found %d error(s) and %d warning(s)', $this->counts['error'], $this->counts['warning'] ), OutputInterface::VERBOSITY_VERBOSE ); |
216
|
|
|
return $this->counts['error'] || $this->counts['warning'] && ! $input->getOption( 'no-strict' ) ? 1 : 0; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
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.