Passed
Push — main ( 95b369...5c384e )
by Michiel
17:35 queued 12s
created

SonarTask   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 397
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 54
eloc 138
dl 0
loc 397
ccs 0
cts 145
cp 0
rs 6.4799
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A validateConfiguration() 0 17 5
B validateProperties() 0 39 6
A init() 0 3 1
B validateDebug() 0 12 8
A addProperty() 0 6 1
A main() 0 21 3
A isWindows() 0 3 1
A setErrors() 0 6 1
B validateExecutable() 0 57 9
A parseConfigurationFile() 0 8 3
A setDebug() 0 6 1
B validateErrors() 0 12 8
A setExecutable() 0 6 1
A setConfiguration() 0 6 1
A constructOptionsString() 0 10 2
A checkExecAllowed() 0 5 3

How to fix   Complexity   

Complex Class

Complex classes like SonarTask often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SonarTask, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing\Task\Ext\Sonar;
22
23
use Phing\Exception\BuildException;
24
use Phing\Project;
25
use Phing\Task;
26
27
/**
28
 * Runs SonarQube Scanner.
29
 *
30
 * @author  Bernhard Mendl <[email protected]>
31
 * @package phing.tasks.ext.sonar
32
 * @see     http://www.sonarqube.org
33
 */
34
class SonarTask extends Task
35
{
36
    public const EXIT_SUCCESS = 0;
37
38
    /**
39
     *
40
     * @var string|null
41
     */
42
    private $executable = null;
43
44
    /**
45
     *
46
     * @var string
47
     */
48
    private $errors = 'false';
49
50
    /**
51
     *
52
     * @var string
53
     */
54
    private $debug = 'false';
55
56
    /**
57
     *
58
     * @var string|null
59
     */
60
    private $configuration = null;
61
62
    /**
63
     *
64
     * @var array Nested *Property* elements.
65
     * @see Property
66
     */
67
    private $propertyElements = [];
68
69
    /**
70
     * The command-line options passed to the SonarQube Scanner executable.
71
     *
72
     * @var array
73
     */
74
    private $commandLineOptions = [];
75
76
    /**
77
     * Map containing SonarQube's "analysis parameters".
78
     *
79
     * Map keys are SonarQube parameter names. Map values are parameter values.
80
     * See {@link http://docs.sonarqube.org/display/SONAR/Analysis+Parameters}.
81
     *
82
     * @var array
83
     */
84
    private $properties = [];
85
86
    /**
87
     * Sets the path of the SonarQube Scanner executable.
88
     *
89
     * If the SonarQube Scanner is included in the PATH environment variable,
90
     * the file name is sufficient.
91
     *
92
     * @param  string $executable
93
     * @return void
94
     */
95
    public function setExecutable($executable)
96
    {
97
        $this->executable = (string) $executable;
98
99
        $message = sprintf("Set executable to [%s].", $this->executable);
100
        $this->log($message, Project::MSG_DEBUG);
101
    }
102
103
    /**
104
     * Sets or unsets the "--errors" flag of SonarQube Scanner.
105
     *
106
     * @param  string $errors
107
     *            Allowed values are "true"/"false", "yes"/"no", or "1"/"0".
108
     * @return void
109
     */
110
    public function setErrors($errors)
111
    {
112
        $this->errors = strtolower((string) $errors);
113
114
        $message = sprintf("Set errors flag to [%s].", $this->errors);
115
        $this->log($message, Project::MSG_DEBUG);
116
    }
117
118
    /**
119
     * Sets or unsets the "--debug" flag of SonarQube Scanner.
120
     *
121
     * @param  string $debug
122
     *            Allowed values are "true"/"false", "yes"/"no", or "1"/"0".
123
     * @return void
124
     */
125
    public function setDebug($debug)
126
    {
127
        $this->debug = strtolower((string) $debug);
128
129
        $message = sprintf("Set debug flag to [%s].", $this->debug);
130
        $this->log($message, Project::MSG_DEBUG);
131
    }
132
133
    /**
134
     * Sets the path of a configuration file for SonarQube Scanner.
135
     *
136
     * @param  string $configuration
137
     * @return void
138
     */
139
    public function setConfiguration($configuration)
140
    {
141
        $this->configuration = (string) $configuration;
142
143
        $message = sprintf("Set configuration to [%s].", $this->configuration);
144
        $this->log($message, Project::MSG_DEBUG);
145
    }
146
147
    /**
148
     * Adds a nested Property element.
149
     *
150
     * @param  SonarProperty $property
151
     * @return void
152
     */
153
    public function addProperty(SonarProperty $property)
154
    {
155
        $this->propertyElements[] = $property;
156
157
        $message = sprintf("Added property: [%s] = [%s].", $property->getName(), $property->getValue());
158
        $this->log($message, Project::MSG_DEBUG);
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     *
164
     * @see Task::init()
165
     */
166
    public function init()
167
    {
168
        $this->checkExecAllowed();
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     *
174
     * @see Task::main()
175
     */
176
    public function main()
177
    {
178
        $this->validateErrors();
179
        $this->validateDebug();
180
        $this->validateConfiguration();
181
        $this->validateProperties();
182
        $this->validateExecutable();
183
184
        $command = sprintf('%s %s', escapeshellcmd($this->executable), $this->constructOptionsString());
0 ignored issues
show
Bug introduced by
It seems like $this->executable can also be of type null; however, parameter $command of escapeshellcmd() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

184
        $command = sprintf('%s %s', escapeshellcmd(/** @scrutinizer ignore-type */ $this->executable), $this->constructOptionsString());
Loading history...
185
186
        $message = sprintf('Executing: [%s]', $command);
187
        $this->log($message, Project::MSG_VERBOSE);
188
189
        exec($command, $output, $returnCode);
190
191
        foreach ($output as $line) {
192
            $this->log($line);
193
        }
194
195
        if ($returnCode !== self::EXIT_SUCCESS) {
196
            throw new BuildException('Execution of SonarQube Scanner failed.');
197
        }
198
    }
199
200
    /**
201
     * Constructs command-line options string for SonarQube Scanner.
202
     *
203
     * @return string
204
     */
205
    private function constructOptionsString()
206
    {
207
        $options = implode(' ', $this->commandLineOptions);
208
209
        foreach ($this->properties as $name => $value) {
210
            $arg = sprintf('%s=%s', $name, $value);
211
            $options .= ' -D ' . escapeshellarg($arg);
212
        }
213
214
        return $options;
215
    }
216
217
    /**
218
     * Check whether PHP function 'exec()' is available.
219
     *
220
     * @throws BuildException
221
     * @return void
222
     */
223
    private function checkExecAllowed()
224
    {
225
        if (!function_exists('exec') || !is_callable('exec')) {
226
            $message = 'Cannot execute SonarQube Scanner because calling PHP function exec() is not permitted by PHP configuration.';
227
            throw new BuildException($message);
228
        }
229
    }
230
231
    /**
232
     *
233
     * @throws BuildException
234
     * @return void
235
     */
236
    private function validateExecutable()
237
    {
238
        if (($this->executable === null) || ($this->executable === '')) {
239
            $message = 'You must specify the path of the SonarQube Scanner using the "executable" attribute.';
240
            throw new BuildException($message);
241
        }
242
243
        // Note that executable is used as argument here.
244
        $escapedExecutable = escapeshellarg($this->executable);
245
246
        if ($this->isWindows()) {
247
            $message = 'Assuming a Windows system. Looking for SonarQube Scanner ...';
248
            $command = 'where ' . $escapedExecutable;
249
        } else {
250
            $message = 'Assuming a Linux or Mac system. Looking for SonarQube Scanner ...';
251
            $command = 'which ' . $escapedExecutable;
252
        }
253
254
        $this->log($message, Project::MSG_VERBOSE);
255
        $output = "";
256
        exec($command, $output, $returnCode);
0 ignored issues
show
Bug introduced by
$output of type string is incompatible with the type array expected by parameter $output of exec(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

256
        exec($command, /** @scrutinizer ignore-type */ $output, $returnCode);
Loading history...
257
258
        if ($returnCode !== self::EXIT_SUCCESS) {
259
            $message = sprintf('Cannot find SonarQube Scanner: [%s].', $this->executable);
260
            throw new BuildException($message);
261
        }
262
263
        // Verify that executable is indeed SonarQube Scanner ...
264
        $escapedExecutable = escapeshellcmd($this->executable);
265
        unset($output);
266
        exec($escapedExecutable . ' --version', $output, $returnCode);
267
268
        if ($returnCode !== self::EXIT_SUCCESS) {
269
            $message = sprintf(
270
                'Could not check version string. Executable appears not to be SonarQube Scanner: [%s].',
271
                $this->executable
272
            );
273
            throw new BuildException($message);
274
        }
275
276
        $isOk = false;
277
        foreach ($output as $line) {
278
            if (preg_match('/Sonar(Qube )?Scanner \d+\\.\d+/', $line) === 1) {
279
                $isOk = true;
280
                break;
281
            }
282
        }
283
284
        if ($isOk) {
285
            $message = sprintf('Found SonarQube Scanner: [%s].', $this->executable);
286
            $this->log($message, Project::MSG_VERBOSE);
287
        } else {
288
            $message = sprintf(
289
                'Could not find name of SonarQube Scanner in version string. Executable appears not to be SonarQube Scanner: [%s].',
290
                $this->executable
291
            );
292
            throw new BuildException($message);
293
        }
294
    }
295
296
    /**
297
     *
298
     * @throws BuildException
299
     * @return void
300
     */
301
    private function validateErrors()
302
    {
303
        if (($this->errors === '1') || ($this->errors === 'true') || ($this->errors === 'yes')) {
304
            $errors = true;
305
        } elseif (($this->errors === '0') || ($this->errors === 'false') || ($this->errors === 'no')) {
306
            $errors = false;
307
        } else {
308
            throw new BuildException('Expected a boolean value.');
309
        }
310
311
        if ($errors) {
312
            $this->commandLineOptions[] = '--errors';
313
        }
314
    }
315
316
    /**
317
     *
318
     * @throws BuildException
319
     * @return void
320
     */
321
    private function validateDebug()
322
    {
323
        if (($this->debug === '1') || ($this->debug === 'true') || ($this->debug === 'yes')) {
324
            $debug = true;
325
        } elseif (($this->debug === '0') || ($this->debug === 'false') || ($this->debug === 'no')) {
326
            $debug = false;
327
        } else {
328
            throw new BuildException('Expected a boolean value.');
329
        }
330
331
        if ($debug) {
332
            $this->commandLineOptions[] = '--debug';
333
        }
334
    }
335
336
    /**
337
     *
338
     * @throws BuildException
339
     * @return void
340
     */
341
    private function validateConfiguration()
342
    {
343
        if (($this->configuration === null) || ($this->configuration === '')) {
344
            // NOTE: Ignore an empty configuration. This allows for
345
            // using Phing properties as attribute values, e.g.
346
            // <sonar ... configuration="{sonar.config.file}">.
347
            return;
348
        }
349
350
        if (!@file_exists($this->configuration)) {
351
            $message = sprintf('Cannot find configuration file [%s].', $this->configuration);
352
            throw new BuildException($message);
353
        }
354
355
        if (!@is_readable($this->configuration)) {
356
            $message = sprintf('Cannot read configuration file [%s].', $this->configuration);
357
            throw new BuildException($message);
358
        }
359
360
        // TODO: Maybe check file type?
361
    }
362
363
    /**
364
     *
365
     * @throws BuildException
366
     * @return void
367
     */
368
    private function validateProperties()
369
    {
370
        $this->properties = $this->parseConfigurationFile();
371
372
        foreach ($this->propertyElements as $property) {
373
            $name = $property->getName();
374
            $value = $property->getValue();
375
376
            if ($name === null || $name === '') {
377
                throw new BuildException('Property name must not be null or empty.');
378
            }
379
380
            if (array_key_exists($name, $this->properties)) {
381
                $message = sprintf(
382
                    'Property [%s] overwritten: old value [%s], new value [%s].',
383
                    $name,
384
                    $this->properties[$name],
385
                    $value
386
                );
387
                $this->log($message, Project::MSG_WARN);
388
            }
389
390
            $this->properties[$name] = $value;
391
        }
392
393
        // Check if all properties required by SonarQube Scanner are set ...
394
        $requiredProperties = [
395
            'sonar.projectKey',
396
            'sonar.projectName',
397
            'sonar.projectVersion',
398
            'sonar.sources'
399
        ];
400
        $intersection = array_intersect($requiredProperties, array_keys($this->properties));
401
        if (count($intersection) < count($requiredProperties)) {
402
            $message = 'Missing some parameters. The following properties are mandatory: ' . implode(
403
                ', ',
404
                $requiredProperties
405
            ) . '.';
406
            throw new BuildException($message);
407
        }
408
    }
409
410
    /**
411
     *
412
     * @return array
413
     */
414
    private function parseConfigurationFile()
415
    {
416
        if (($this->configuration === null) || ($this->configuration === '')) {
417
            return [];
418
        }
419
420
        $parser = new SonarConfigurationFileParser($this->configuration, $this->project);
421
        return $parser->parse();
422
    }
423
424
    /**
425
     *
426
     * @return boolean
427
     */
428
    private function isWindows()
429
    {
430
        return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
431
    }
432
}
433