Passed
Push — main ( b7649a...398f47 )
by Michiel
06:32
created

JslLintTask::getExecutable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
namespace Phing\Task\Optional;
21
22
use Phing\Exception\BuildException;
23
use Phing\Io\FileWriter;
24
use Phing\Io\File;
25
use Phing\Project;
26
use Phing\Task;
27
use Phing\Type\Element\FileSetAware;
28
use Phing\Util\DataStore;
29
use Phing\Util\StringHelper;
30
31
/**
32
 * A Javascript lint task. Checks syntax of Javascript files.
33
 * Javascript lint (http://www.javascriptlint.com) must be in the system path.
34
 * This class is based on Knut Urdalen's PhpLintTask.
35
 *
36
 * @author Stefan Priebsch <[email protected]>
37
 *
38
 * @package phing.tasks.ext
39
 */
40
class JslLintTask extends Task
41
{
42
    use FileSetAware;
43
44
    /**
45
     * @var File
46
     */
47
    protected $file; // the source file (from xml attribute)
48
49
    /**
50
     * @var bool $showWarnings
51
     */
52
    protected $showWarnings = true;
53
54
    /**
55
     * @var bool
56
     */
57
    protected $haltOnFailure = false;
58
59
    /**
60
     * @var bool
61
     */
62
    protected $haltOnWarning = false;
63
64
    /**
65
     * @var bool
66
     */
67
    protected $hasErrors = false;
68
69
    /**
70
     * @var bool
71
     */
72
    protected $hasWarnings = false;
73
74
    /**
75
     * @var array $badFiles
76
     */
77
    private $badFiles = [];
78
79
    /**
80
     * @var DataStore
81
     */
82
    private $cache = null;
83
84
    /**
85
     * @var File
86
     */
87
    private $conf = null;
88
89
    /**
90
     * @var string
91
     */
92
    private $executable = "jsl";
93
94
    /**
95
     * @var File
96
     */
97
    protected $tofile = null;
98
99
    /**
100
     * Sets the flag if warnings should be shown
101
     *
102
     * @param boolean $show
103
     */
104
    public function setShowWarnings($show)
105
    {
106
        $this->showWarnings = StringHelper::booleanValue($show);
107
    }
108
109
    /**
110
     * The haltonfailure property
111
     *
112
     * @param boolean $aValue
113
     */
114
    public function setHaltOnFailure($aValue)
115
    {
116
        $this->haltOnFailure = $aValue;
117
    }
118
119
    /**
120
     * The haltonwarning property
121
     *
122
     * @param boolean $aValue
123
     */
124
    public function setHaltOnWarning($aValue)
125
    {
126
        $this->haltOnWarning = $aValue;
127
    }
128
129
    /**
130
     * File to be performed syntax check on
131
     *
132
     * @param File $file
133
     */
134
    public function setFile(File $file)
135
    {
136
        $this->file = $file;
137
    }
138
139
    /**
140
     * Whether to store last-modified times in cache
141
     *
142
     * @param File $file
143
     */
144
    public function setCacheFile(File $file)
145
    {
146
        $this->cache = new DataStore($file);
147
    }
148
149
    /**
150
     * jsl config file
151
     *
152
     * @param File $file
153
     */
154
    public function setConfFile(File $file)
155
    {
156
        $this->conf = $file;
157
    }
158
159
    /**
160
     * @param string $path
161
     *
162
     * @throws BuildException
163
     */
164
    public function setExecutable($path)
165
    {
166
        $this->executable = $path;
167
168
        if (!@file_exists($path)) {
169
            throw new BuildException("JavaScript Lint executable '{$path}' not found");
170
        }
171
    }
172
173
    /**
174
     * @return string
175
     */
176
    public function getExecutable()
177
    {
178
        return $this->executable;
179
    }
180
181
    /**
182
     * File to save error messages to
183
     *
184
     * @param File $tofile
185
     */
186
    public function setToFile(File $tofile)
187
    {
188
        $this->tofile = $tofile;
189
    }
190
191
    /**
192
     * Execute lint check against PhingFile or a FileSet
193
     */
194
    public function main()
195
    {
196
        if (!isset($this->file) and count($this->filesets) == 0) {
197
            throw new BuildException("Missing either a nested fileset or attribute 'file' set");
198
        }
199
200
        if (empty($this->executable)) {
201
            throw new BuildException("Missing the 'executable' attribute");
202
        }
203
204
        if ($this->file instanceof File) {
0 ignored issues
show
introduced by
$this->file is always a sub-type of Phing\Io\File.
Loading history...
205
            $this->lint($this->file->getPath());
206
        } else { // process filesets
207
            $project = $this->getProject();
208
            foreach ($this->filesets as $fs) {
209
                $ds = $fs->getDirectoryScanner($project);
210
                $files = $ds->getIncludedFiles();
211
                $dir = $fs->getDir($this->project)->getPath();
212
                foreach ($files as $file) {
213
                    $this->lint($dir . DIRECTORY_SEPARATOR . $file);
214
                }
215
            }
216
        }
217
218
        // write list of 'bad files' to file (if specified)
219
        if ($this->tofile) {
220
            $writer = new FileWriter($this->tofile);
221
222
            foreach ($this->badFiles as $file => $messages) {
223
                foreach ($messages as $msg) {
224
                    $writer->write($file . "=" . $msg . PHP_EOL);
225
                }
226
            }
227
228
            $writer->close();
229
        }
230
231
        if ($this->haltOnFailure && $this->hasErrors) {
232
            throw new BuildException(
233
                'Syntax error(s) in JS files:' . implode(
234
                    ', ',
235
                    array_keys($this->badFiles)
236
                )
237
            );
238
        }
239
        if ($this->haltOnWarning && $this->hasWarnings) {
240
            throw new BuildException(
241
                'Syntax warning(s) in JS files:' . implode(
242
                    ', ',
243
                    array_keys($this->badFiles)
244
                )
245
            );
246
        }
247
    }
248
249
    /**
250
     * Performs the actual syntax check
251
     *
252
     * @param string $file
253
     *
254
     * @return bool|void
255
     * @throws BuildException
256
     *
257
     */
258
    protected function lint($file)
259
    {
260
        $command = $this->executable . ' -output-format ' . escapeshellarg(
261
            'file:__FILE__;line:__LINE__;message:__ERROR__'
262
        ) . ' ';
263
264
        if (isset($this->conf)) {
265
            $command .= '-conf ' . escapeshellarg($this->conf->getPath()) . ' ';
266
        }
267
268
        $command .= '-process ';
269
270
        if (file_exists($file)) {
271
            if (is_readable($file)) {
272
                if ($this->cache) {
273
                    $lastmtime = $this->cache->get($file);
274
275
                    if ($lastmtime >= filemtime($file)) {
276
                        $this->log("Not linting '" . $file . "' due to cache", Project::MSG_DEBUG);
277
278
                        return false;
279
                    }
280
                }
281
282
                $messages = [];
283
                exec($command . '"' . $file . '"', $messages, $return);
284
285
                if ($return > 100) {
286
                    throw new BuildException("Could not execute Javascript Lint executable '{$this->executable}'");
287
                }
288
289
                $summary = $messages[count($messages) - 1];
290
291
                preg_match('/(\d+)\serror/', $summary, $matches);
292
                $errorCount = (count($matches) > 1 ? $matches[1] : 0);
293
294
                preg_match('/(\d+)\swarning/', $summary, $matches);
295
                $warningCount = (count($matches) > 1 ? $matches[1] : 0);
296
297
                $errors = [];
298
                $warnings = [];
299
                if ($errorCount > 0 || $warningCount > 0) {
300
                    $last = false;
301
                    foreach ($messages as $message) {
302
                        $matches = [];
303
                        if (preg_match('/^(\.*)\^$/', $message)) {
304
                            $column = strlen($message);
305
                            if ($last == 'error') {
306
                                $errors[count($errors) - 1]['column'] = $column;
307
                            } else {
308
                                if ($last == 'warning') {
309
                                    $warnings[count($warnings) - 1]['column'] = $column;
310
                                }
311
                            }
312
                            $last = false;
313
                        }
314
                        if (!preg_match('/^file:(.+);line:(\d+);message:(.+)$/', $message, $matches)) {
315
                            continue;
316
                        }
317
                        $msg = $matches[3];
318
                        $data = ['filename' => $matches[1], 'line' => $matches[2], 'message' => $msg];
319
                        if (preg_match('/^.*error:.+$/i', $msg)) {
320
                            $errors[] = $data;
321
                            $last = 'error';
322
                        } else {
323
                            if (preg_match('/^.*warning:.+$/i', $msg)) {
324
                                $warnings[] = $data;
325
                                $last = 'warning';
326
                            }
327
                        }
328
                    }
329
                }
330
331
                if ($this->showWarnings && $warningCount > 0) {
332
                    $this->log($file . ': ' . $warningCount . ' warnings detected', Project::MSG_WARN);
333
                    foreach ($warnings as $warning) {
334
                        $this->log(
335
                            '- line ' . $warning['line'] . (isset($warning['column']) ? ' column ' . $warning['column'] : '') . ': ' . $warning['message'],
336
                            Project::MSG_WARN
337
                        );
338
                    }
339
                    $this->hasWarnings = true;
340
                }
341
342
                if ($errorCount > 0) {
343
                    $this->log($file . ': ' . $errorCount . ' errors detected', Project::MSG_ERR);
344
                    if (!isset($this->badFiles[$file])) {
345
                        $this->badFiles[$file] = [];
346
                    }
347
348
                    foreach ($errors as $error) {
349
                        $message = 'line ' . $error['line'] . (isset($error['column']) ? ' column ' . $error['column'] : '') . ': ' . $error['message'];
350
                        $this->log('- ' . $message, Project::MSG_ERR);
351
                        $this->badFiles[$file][] = $message;
352
                    }
353
                    $this->hasErrors = true;
354
                } else {
355
                    if (!$this->showWarnings || $warningCount == 0) {
356
                        $this->log($file . ': No syntax errors detected', Project::MSG_VERBOSE);
357
358
                        if ($this->cache) {
359
                            $this->cache->put($file, filemtime($file));
360
                        }
361
                    }
362
                }
363
            } else {
364
                throw new BuildException('Permission denied: ' . $file);
365
            }
366
        } else {
367
            throw new BuildException('File not found: ' . $file);
368
        }
369
    }
370
}
371