Page MenuHomePhabricator

D14535.diff
No OneTemporary

D14535.diff

Index: src/__phutil_library_map__.php
===================================================================
--- src/__phutil_library_map__.php
+++ src/__phutil_library_map__.php
@@ -98,6 +98,7 @@
'ArcanistEventType' => 'events/constant/ArcanistEventType.php',
'ArcanistExitExpressionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExitExpressionXHPASTLinterRule.php',
'ArcanistExportWorkflow' => 'workflow/ArcanistExportWorkflow.php',
+ 'ArcanistExternalJsonLinter' => 'lint/linter/ArcanistExternalJsonLinter.php',
'ArcanistExternalLinter' => 'lint/linter/ArcanistExternalLinter.php',
'ArcanistExternalLinterTestCase' => 'lint/linter/__tests__/ArcanistExternalLinterTestCase.php',
'ArcanistExtractUseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php',
@@ -393,6 +394,7 @@
'ArcanistEventType' => 'PhutilEventType',
'ArcanistExitExpressionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistExportWorkflow' => 'ArcanistWorkflow',
+ 'ArcanistExternalJsonLinter' => 'ArcanistLinter',
'ArcanistExternalLinter' => 'ArcanistFutureLinter',
'ArcanistExternalLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
Index: src/lint/linter/ArcanistExternalJsonLinter.php
===================================================================
--- /dev/null
+++ src/lint/linter/ArcanistExternalJsonLinter.php
@@ -0,0 +1,272 @@
+<?php
+
+/**
+ * Simple glue linter which runs some script on each path and parses
+ * lint violations emitted by the script in JSON format.
+ *
+ * Configure this linter by setting these keys in your .arclint section:
+ *
+ * - `external-json.script` Script command to run. This can be
+ * the path to a linter script, but may also include flags or use shell
+ * features (see below for examples).
+ *
+ * The script will be invoked from the project root, so you can specify a
+ * relative path like `scripts/lint.sh` or an absolute path like
+ * `/opt/lint/lint.sh`.
+ *
+ * This linter is necessarily more limited in its capabilities than a normal
+ * linter which can perform custom processing, but may be somewhat simpler to
+ * configure.
+ *
+ * == Script and JSON format ==
+ *
+ * The script will be invoked once for each file that is to be linted, with
+ * the file passed as the first argument. The file may begin with a "-"; ensure
+ * your script will not interpret such files as flags (perhaps by ending your
+ * script configuration with "--", if its argument parser supports that).
+ *
+ * Note that when run via `arc diff`, the list of files to be linted includes
+ * deleted files and files that were moved away by the change. The linter should
+ * not assume the path it is given exists, and it is not an error for the
+ * linter to be invoked with paths which are no longer there. (Every affected
+ * path is subject to lint because some linters may raise errors in other files
+ * when a file is removed, or raise an error about its removal.)
+ *
+ * The script should emit a JSON array of lint violations to stdout. A lint
+ * violation may have the following attributes,
+ *
+ * - `message` (required) Text describing the lint message. For example,
+ * "This is a syntax error.".
+ * - `name` (optional) Text summarizing the lint message. For example,
+ * "Syntax Error".
+ * - `severity` (optional) The word "error", "warning", "autofix", "advice",
+ * or "disabled", in any combination of upper and lower case. Instead, you
+ * - `file` (optional) The name of the file to raise the lint message in. If
+ * not specified, defaults to the linted file. It is generally not necessary
+ * to specify this unless the linter can raise messages in files other than
+ * the one it is linting.
+ * - `line` (optional) The line number of the message.
+ * - `char` (optional) The character offset of the message.
+ * - `offset` (optional) The byte offset of the message. If provided, this
+ * supersedes `line` and `char`.
+ * - `original` (optional) The text the message affects.
+ * - `replacement` (optional) The text that the range captured by `original`
+ * should be automatically replaced by to resolve the message.
+ * - `code` (optional) A short error type identifier which can be used
+ * elsewhere to configure handling of specific types of messages. For
+ * example, "EXAMPLE1", "EXAMPLE2", etc., where each code identifies a
+ * class of message like "syntax error", "missing whitespace", etc. This
+ * allows configuration to later change the severity of all whitespace
+ * messages, for example.
+ * - `throw` (optional) If set with a string error message `arc` will throw
+ * the given message. You can use this to fail abruptly if you
+ * encounter unexpected output. All processing will abort.
+ *
+ * For example, the following would encode a warning and an error,
+ *
+ * [ { 'message': 'Too many goats!', 'line': 13, 'severity': 'error' }
+ * , { 'message': 'Not enough boats.', 'line': 22, 'severity: 'warning' }
+ * ]
+ *
+ * @task lint Linting
+ * @task linterinfo Linter Information
+ * @task parse Parsing Output
+ * @task config Validating Configuration
+ */
+final class ArcanistExternalJsonLinter extends ArcanistLinter {
+
+ private $script = null;
+ private $output = array();
+
+ public function getInfoName() {
+ return pht('External JSON');
+ }
+
+ public function getInfoDescription() {
+ return pht(
+ 'Run an external script, then parse its output as a JSON document'.
+ 'describing the lint violations. This is a generic binding that can '.
+ 'be used to run custom lint scripts.');
+ }
+
+ protected function shouldLintBinaryFiles() {
+ return true;
+ }
+
+ protected function shouldLintDeletedFiles() {
+ return true;
+ }
+
+ protected function shouldLintDirectories() {
+ return true;
+ }
+
+ protected function shouldLintSymbolicLinks() {
+ return true;
+ }
+
+
+/* -( Linting )------------------------------------------------------------ */
+
+
+ /**
+ * Run the script on each file to be linted.
+ *
+ * @task lint
+ */
+ public function willLintPaths(array $paths) {
+ $root = $this->getProjectRoot();
+
+ $futures = array();
+ foreach ($paths as $path) {
+ $future = new ExecFuture('%C %s', $this->script, $path);
+ $future->setCWD($root);
+ $futures[$path] = $future;
+ }
+
+ $futures = id(new FutureIterator($futures))
+ ->limit(4);
+ foreach ($futures as $path => $future) {
+ list($stdout) = $future->resolvex();
+ $this->output[$path] = $stdout;
+ }
+ }
+
+ /**
+ * Run the regex on the output of the script.
+ *
+ * @task lint
+ */
+ public function lintPath($path) {
+ $output = idx($this->output, $path);
+ if (!strlen($output)) {
+ // No output, but it exited 0, so just move on.
+ return;
+ }
+
+ $messages = json_decode($output, true);
+
+ foreach ($messages as $message) {
+ if (!empty($message['throw'])) {
+ $throw = $message['throw'];
+ throw new ArcanistUsageException(
+ pht(
+ "%s: linter threw an exception: '%s'\n",
+ __CLASS__,
+ $throw));
+ }
+
+ $line = idx($message, 'line');
+ if ($line) {
+ $line = (int)$line;
+ } else {
+ $line = null;
+ }
+ $char = idx($message, 'char');
+ if ($char) {
+ $char = (int)$char;
+ } else {
+ $char = null;
+ }
+
+ $dict = array(
+ 'path' => idx($message, 'file', $path),
+ 'line' => $line,
+ 'char' => $char,
+ 'code' => idx($message, 'code', $this->getLinterName()),
+ 'severity' => $this->getMessageSeverity($message),
+ 'name' => idx($message, 'name', 'Lint'),
+ 'description' => idx($message, 'message',
+ pht('Undefined Lint Message')),
+ );
+
+ $original = idx($message, 'original');
+ if ($original !== null) {
+ $dict['original'] = $original;
+ }
+
+ $replacement = idx($message, 'replacement');
+ if ($replacement !== null) {
+ $dict['replacement'] = $replacement;
+ }
+
+ $lint = ArcanistLintMessage::newFromDictionary($dict);
+ $this->addLintMessage($lint);
+ }
+ }
+
+
+/* -( Linter Information )------------------------------------------------- */
+
+ /**
+ * Return the short name of the linter.
+ *
+ * @return string Short linter identifier.
+ *
+ * @task linterinfo
+ */
+ public function getLinterName() {
+ return 'ExtJson';
+ }
+
+ public function getLinterConfigurationName() {
+ return 'external-json';
+ }
+
+ public function getLinterConfigurationOptions() {
+ // These fields are optional only to avoid breaking things.
+ $options = array(
+ 'external-json.script' => array(
+ 'type' => 'string',
+ 'help' => pht('Script to execute.'),
+ ),
+ );
+
+ return $options + parent::getLinterConfigurationOptions();
+ }
+
+ public function setLinterConfigurationValue($key, $value) {
+ switch ($key) {
+ case 'external-json.script':
+ $this->script = $value;
+ return;
+ }
+
+ return parent::setLinterConfigurationValue($key, $value);
+ }
+
+/* -( Parsing Output )----------------------------------------------------- */
+
+ /**
+ * Map the regex matching groups to a message severity. We look for either
+ * a nonempty severity name group like 'error', or a group called 'severity'
+ * with a valid name.
+ *
+ * @param dict message object
+ * @return const @{class:ArcanistLintSeverity} constant.
+ *
+ * @task parse
+ */
+ private function getMessageSeverity(array $message) {
+ $map = array(
+ 'error' => ArcanistLintSeverity::SEVERITY_ERROR,
+ 'warning' => ArcanistLintSeverity::SEVERITY_WARNING,
+ 'autofix' => ArcanistLintSeverity::SEVERITY_AUTOFIX,
+ 'advice' => ArcanistLintSeverity::SEVERITY_ADVICE,
+ 'disabled' => ArcanistLintSeverity::SEVERITY_DISABLED,
+ );
+
+ if (idx($message, 'severity')) {
+ $severity_name = strtolower(idx($message, 'severity'));
+ if (!idx($map, $severity_name)) {
+ throw new ArcanistUsageException(
+ pht('%s: Unknown severity %s', __CLASS__, $severity_name));
+ } else {
+ return $map[$severity_name];
+ }
+ } else {
+ return ArcanistLintSeverity::SEVERITY_ERROR;
+ }
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Sun, May 12, 1:58 AM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6275980
Default Alt Text
D14535.diff (10 KB)

Event Timeline