4
* Base class for linters which operate by invoking an external program and
7
* @task bin Interpreters, Binaries and Flags
8
* @task parse Parsing Linter Output
9
* @task exec Executing the Linter
11
abstract class ArcanistExternalLinter extends ArcanistFutureLinter {
18
/* -( Interpreters, Binaries and Flags )----------------------------------- */
21
* Return the default binary name or binary path where the external linter
22
* lives. This can either be a binary name which is expected to be installed
23
* in PATH (like "jshint"), or a relative path from the project root
24
* (like "resources/support/bin/linter") or an absolute path.
26
* If the binary needs an interpreter (like "python" or "node"), you should
27
* also override @{method:shouldUseInterpreter} and provide the interpreter
28
* in @{method:getDefaultInterpreter}.
30
* @return string Default binary to execute.
33
abstract public function getDefaultBinary();
36
* Return a human-readable string describing how to install the linter. This
37
* is normally something like "Install such-and-such by running `npm install
38
* -g such-and-such`.", but will differ from linter to linter.
40
* @return string Human readable install instructions
43
abstract public function getInstallInstructions();
46
* Return true to continue when the external linter exits with an error code.
47
* By default, linters which exit with an error code are assumed to have
48
* failed. However, some linters exit with a specific code to indicate that
49
* lint messages were detected.
51
* If the linter sometimes raises errors during normal operation, override
52
* this method and return true so execution continues when it exits with
55
* @param bool Return true to continue on nonzero error code.
58
public function shouldExpectCommandErrors() {
63
* Return true to indicate that the external linter can read input from
64
* stdin, rather than requiring a file. If this mode is supported, it is
65
* slightly more flexible and may perform better, and is thus preferable.
67
* To send data over stdin instead of via a command line parameter, override
68
* this method and return true. If the linter also needs a command line
69
* flag (like `--stdin` or `-`), override
70
* @{method:getReadDataFromStdinFilename} to provide it.
72
* For example, linters are normally invoked something like this:
76
* If you override this method, invocation will be more similar to this:
80
* If you additionally override @{method:getReadDataFromStdinFilename} to
81
* return `"-"`, invocation will be similar to this:
83
* $ linter - < file.js
85
* @return bool True to send data over stdin.
88
public function supportsReadDataFromStdin() {
93
* If the linter can read data over stdin, override
94
* @{method:supportsReadDataFromStdin} and then optionally override this
95
* method to provide any required arguments (like `-` or `--stdin`). See
96
* that method for discussion.
98
* @return string|null Additional arguments required by the linter when
99
* operating in stdin mode.
102
public function getReadDataFromStdinFilename() {
107
* Provide mandatory, non-overridable flags to the linter. Generally these
108
* are format flags, like `--format=xml`, which must always be given for
109
* the output to be usable.
111
* Flags which are not mandatory should be provided in
112
* @{method:getDefaultFlags} instead.
114
* @return list<string> Mandatory flags, like `"--format=xml"`.
117
protected function getMandatoryFlags() {
122
* Provide default, overridable flags to the linter. Generally these are
123
* configuration flags which affect behavior but aren't critical. Flags
124
* which are required should be provided in @{method:getMandatoryFlags}
127
* Default flags can be overridden with @{method:setFlags}.
129
* @return list<string> Overridable default flags.
132
protected function getDefaultFlags() {
137
* Override default flags with custom flags. If not overridden, flags provided
138
* by @{method:getDefaultFlags} are used.
140
* @param list<string> New flags.
144
final public function setFlags($flags) {
145
$this->flags = (array)$flags;
150
* Return the binary or script to execute. This method synthesizes defaults
151
* and configuration. You can override the binary with @{method:setBinary}.
153
* @return string Binary to execute.
156
final public function getBinary() {
157
return coalesce($this->bin, $this->getDefaultBinary());
161
* Override the default binary with a new one.
163
* @param string New binary.
167
final public function setBinary($bin) {
173
* Return true if this linter should use an interpreter (like "python" or
174
* "node") in addition to the script.
176
* After overriding this method to return `true`, override
177
* @{method:getDefaultInterpreter} to set a default.
179
* @return bool True to use an interpreter.
182
public function shouldUseInterpreter() {
187
* Return the default interpreter, like "python" or "node". This method is
188
* only invoked if @{method:shouldUseInterpreter} has been overridden to
191
* @return string Default interpreter.
194
public function getDefaultInterpreter() {
195
throw new Exception('Incomplete implementation!');
199
* Get the effective interpreter. This method synthesizes configuration and
202
* @return string Effective interpreter.
205
final public function getInterpreter() {
206
return coalesce($this->interpreter, $this->getDefaultInterpreter());
210
* Set the interpreter, overriding any default.
212
* @param string New interpreter.
216
final public function setInterpreter($interpreter) {
217
$this->interpreter = $interpreter;
222
/* -( Parsing Linter Output )---------------------------------------------- */
225
* Parse the output of the external lint program into objects of class
226
* @{class:ArcanistLintMessage} which `arc` can consume. Generally, this
227
* means examining the output and converting each warning or error into a
230
* If parsing fails, returning `false` will cause the caller to throw an
231
* appropriate exception. (You can also throw a more specific exception if
232
* you're able to detect a more specific condition.) Otherwise, return a list
235
* @param string Path to the file being linted.
236
* @param int Exit code of the linter.
237
* @param string Stdout of the linter.
238
* @param string Stderr of the linter.
239
* @return list<ArcanistLintMessage>|false List of lint messages, or false
240
* to indicate parser failure.
243
abstract protected function parseLinterOutput($path, $err, $stdout, $stderr);
246
/* -( Executing the Linter )----------------------------------------------- */
249
* Check that the binary and interpreter (if applicable) exist, and throw
250
* an exception with a message about how to install them if they do not.
254
final public function checkBinaryConfiguration() {
256
if ($this->shouldUseInterpreter()) {
257
$interpreter = $this->getInterpreter();
260
$binary = $this->getBinary();
262
// NOTE: If we have an interpreter, we don't require the script to be
263
// executable (so we just check that the path exists). Otherwise, the
264
// binary must be executable.
267
if (!Filesystem::binaryExists($interpreter)) {
268
throw new ArcanistUsageException(
270
'Unable to locate interpreter "%s" to run linter %s. You may '.
271
'need to install the interpreter, or adjust your linter '.
276
$this->getInstallInstructions()));
278
if (!Filesystem::pathExists($binary)) {
279
throw new ArcanistUsageException(
281
'Unable to locate script "%s" to run linter %s. You may need '.
282
'to install the script, or adjust your linter configuration. '.
286
$this->getInstallInstructions()));
289
if (!Filesystem::binaryExists($binary)) {
290
throw new ArcanistUsageException(
292
'Unable to locate binary "%s" to run linter %s. You may need '.
293
'to install the binary, or adjust your linter configuration. '.
297
$this->getInstallInstructions()));
303
* Get the composed executable command, including the interpreter and binary
304
* but without flags or paths. This can be used to execute `--version`
307
* @return string Command to execute the raw linter.
310
final protected function getExecutableCommand() {
311
$this->checkBinaryConfiguration();
314
if ($this->shouldUseInterpreter()) {
315
$interpreter = $this->getInterpreter();
318
$binary = $this->getBinary();
321
$bin = csprintf('%s %s', $interpreter, $binary);
323
$bin = csprintf('%s', $binary);
330
* Get the composed flags for the executable, including both mandatory and
333
* @return list<string> Composed flags.
336
final protected function getCommandFlags() {
337
$mandatory_flags = $this->getMandatoryFlags();
338
if (!is_array($mandatory_flags)) {
340
'String support for flags.', 'You should use list<string> instead.');
341
$mandatory_flags = (array) $mandatory_flags;
344
$flags = nonempty($this->flags, $this->getDefaultFlags());
345
if (!is_array($flags)) {
347
'String support for flags.', 'You should use list<string> instead.');
348
$flags = (array) $flags;
351
return array_merge($mandatory_flags, $flags);
354
public function getCacheVersion() {
355
$version = $this->getVersion();
358
return $version.'-'.json_encode($this->getCommandFlags());
360
// Either we failed to parse the version number or the `getVersion`
361
// function hasn't been implemented.
362
return json_encode($this->getCommandFlags());
367
* Prepare the path to be added to the command string.
369
* This method is expected to return an already escaped string.
371
* @param string Path to the file being linted
372
* @return string The command-ready file argument
374
protected function getPathArgumentForLinterFuture($path) {
375
return csprintf('%s', $path);
378
final protected function buildFutures(array $paths) {
379
$executable = $this->getExecutableCommand();
381
$bin = csprintf('%C %Ls', $executable, $this->getCommandFlags());
384
foreach ($paths as $path) {
385
if ($this->supportsReadDataFromStdin()) {
386
$future = new ExecFuture(
389
$this->getReadDataFromStdinFilename());
390
$future->write($this->getEngine()->loadData($path));
392
// TODO: In commit hook mode, we need to do more handling here.
393
$disk_path = $this->getEngine()->getFilePathOnDisk($path);
394
$path_argument = $this->getPathArgumentForLinterFuture($disk_path);
395
$future = new ExecFuture('%C %C', $bin, $path_argument);
398
$future->setCWD($this->getEngine()->getWorkingCopy()->getProjectRoot());
399
$futures[$path] = $future;
405
final protected function resolveFuture($path, Future $future) {
406
list($err, $stdout, $stderr) = $future->resolve();
407
if ($err && !$this->shouldExpectCommandErrors()) {
411
$messages = $this->parseLinterOutput($path, $err, $stdout, $stderr);
413
if ($messages === false) {
418
"Linter failed to parse output!\n\n{$stdout}\n\n{$stderr}");
422
foreach ($messages as $message) {
423
$this->addLintMessage($message);
427
public function getLinterConfigurationOptions() {
430
'type' => 'optional string | list<string>',
432
'Specify a string (or list of strings) identifying the binary '.
433
'which should be invoked to execute this linter. This overrides '.
434
'the default binary. If you provide a list of possible binaries, '.
435
'the first one which exists will be used.'),
438
'type' => 'optional list<string>',
440
'Provide a list of additional flags to pass to the linter on the '.
445
if ($this->shouldUseInterpreter()) {
446
$options['interpreter'] = array(
447
'type' => 'optional string | list<string>',
449
'Specify a string (or list of strings) identifying the interpreter '.
450
'which should be used to invoke the linter binary. If you provide '.
451
'a list of possible interpreters, the first one that exists '.
456
return $options + parent::getLinterConfigurationOptions();
459
public function setLinterConfigurationValue($key, $value) {
462
$working_copy = $this->getEngine()->getWorkingCopy();
463
$root = $working_copy->getProjectRoot();
465
foreach ((array)$value as $path) {
466
if (Filesystem::binaryExists($path)) {
467
$this->setInterpreter($path);
471
$path = Filesystem::resolvePath($path, $root);
473
if (Filesystem::binaryExists($path)) {
474
$this->setInterpreter($path);
480
pht('None of the configured interpreters can be located.'));
482
$is_script = $this->shouldUseInterpreter();
484
$working_copy = $this->getEngine()->getWorkingCopy();
485
$root = $working_copy->getProjectRoot();
487
foreach ((array)$value as $path) {
488
if (!$is_script && Filesystem::binaryExists($path)) {
489
$this->setBinary($path);
493
$path = Filesystem::resolvePath($path, $root);
494
if ((!$is_script && Filesystem::binaryExists($path)) ||
495
($is_script && Filesystem::pathExists($path))) {
496
$this->setBinary($path);
502
pht('None of the configured binaries can be located.'));
504
if (!is_array($value)) {
506
'String support for flags.',
507
'You should use list<string> instead.');
508
$value = (array) $value;
510
$this->setFlags($value);
514
return parent::setLinterConfigurationValue($key, $value);
518
* Map a configuration lint code to an `arc` lint code. Primarily, this is
519
* intended for validation, but can also be used to normalize case or
520
* otherwise be more permissive in accepted inputs.
522
* If the code is not recognized, you should throw an exception.
524
* @param string Code specified in configuration.
525
* @return string Normalized code to use in severity map.
527
protected function getLintCodeFromLinterConfigurationKey($code) {