190 lines
7.6 KiB
EmacsLisp
190 lines
7.6 KiB
EmacsLisp
;;; flymake-eslint.el --- A Flymake backend for Javascript using eslint -*- lexical-binding: t; -*-
|
|
|
|
;;; Version: 1.5.0
|
|
;; Package-Version: 20191129.1558
|
|
|
|
;;; Author: Dan Orzechowski
|
|
;;; Contributor: Terje Larsen
|
|
|
|
;;; URL: https://github.com/orzechowskid/flymake-eslint
|
|
|
|
;;; Package-Requires: ((emacs "26.0"))
|
|
|
|
;;; Commentary:
|
|
;; A backend for Flymake which uses eslint. Enable it with `M-x flymake-eslint-enable [RET]'.
|
|
;; Alternately, configure a mode-hook for your Javascript major mode of choice:
|
|
;;
|
|
;; (add-hook 'some-js-major-mode-hook
|
|
;; (lambda () (flymake-eslint-enable))
|
|
;;
|
|
;; A handful of configurable options can be found in the `flymake-eslint' customization group: view and modify them with the command `M-x customize-group [RET] flymake-eslint [RET]'.
|
|
|
|
;;; License: MIT
|
|
|
|
;;; Code:
|
|
|
|
|
|
;; our own customization group
|
|
|
|
|
|
(defgroup flymake-eslint nil
|
|
"Flymake checker for Javascript using eslint"
|
|
:group 'programming
|
|
:prefix "flymake-eslint-")
|
|
|
|
|
|
;; useful variables
|
|
|
|
|
|
(defcustom flymake-eslint-executable-name "eslint"
|
|
"Name of executable to run when checker is called. Must be present in variable `exec-path'."
|
|
:type 'string
|
|
:group 'flymake-eslint)
|
|
|
|
(defcustom flymake-eslint-executable-args nil
|
|
"Extra arguments to pass to eslint."
|
|
:type 'string
|
|
:group 'flymake-eslint)
|
|
|
|
(defcustom flymake-eslint-show-rule-name t
|
|
"Set to t to append rule name to end of warning or error message, nil otherwise."
|
|
:type 'boolean
|
|
:group 'flymake-eslint)
|
|
|
|
(defcustom flymake-eslint-defer-binary-check nil
|
|
"Set to t to bypass the initial check which ensures eslint is present.
|
|
|
|
Useful when the value of variable `exec-path' is set dynamically and the location of eslint might not be known ahead of time."
|
|
:type 'boolean
|
|
:group 'flymake-eslint)
|
|
|
|
|
|
;; useful buffer-local variables
|
|
|
|
|
|
(defcustom flymake-eslint-project-root nil
|
|
"Buffer-local. Set to a filesystem path to use that path as the current working directory of the linting process."
|
|
:type 'string
|
|
:group 'flymake-eslint)
|
|
|
|
|
|
;; internal variables
|
|
|
|
|
|
(defvar flymake-eslint--message-regex "^[[:space:]]*\\([0-9]+\\):\\([0-9]+\\)[[:space:]]+\\(warning\\|error\\)[[:space:]]+\\(.+?\\)[[:space:]]\\{2,\\}\\(.*\\)$"
|
|
"Internal variable.
|
|
Regular expression definition to match eslint messages.")
|
|
|
|
(defvar-local flymake-eslint--process nil
|
|
"Internal variable.
|
|
Handle to the linter process for the current buffer.")
|
|
|
|
|
|
;; internal functions
|
|
|
|
|
|
(defun flymake-eslint--ensure-binary-exists ()
|
|
"Internal function.
|
|
Throw an error and tell REPORT-FN to disable itself if `flymake-eslint-executable-name' can't be found on variable `exec-path'"
|
|
(unless (executable-find flymake-eslint-executable-name)
|
|
(error (message "can't find '%s' in exec-path - try M-x set-variable flymake-eslint-executable-name maybe?" flymake-eslint-executable-name))))
|
|
|
|
(defun flymake-eslint--report (eslint-stdout-buffer source-buffer)
|
|
"Internal function.
|
|
Create Flymake diag messages from contents of ESLINT-STDOUT-BUFFER, to be reported against SOURCE-BUFFER. Returns a list of results"
|
|
(with-current-buffer eslint-stdout-buffer
|
|
;; start at the top and check each line for an eslint message
|
|
(goto-char (point-min))
|
|
(if (looking-at-p "Error:")
|
|
(let ((diag (flymake-make-diagnostic source-buffer (point-min) (point-max) :error (thing-at-point 'line t))))
|
|
;; ehhhhh point-min and point-max here are of the eslint output buffer
|
|
;; containing the error message, not source-buffer
|
|
(list diag))
|
|
(let ((results '()))
|
|
(while (not (eobp))
|
|
(when (looking-at flymake-eslint--message-regex)
|
|
(let* ((row (string-to-number (match-string 1)))
|
|
(column (string-to-number (match-string 2)))
|
|
(type (match-string 3))
|
|
(msg (match-string 4))
|
|
(lint-rule (match-string 5))
|
|
(msg-text (if flymake-eslint-show-rule-name
|
|
(format "%s: %s [%s]" type msg lint-rule)
|
|
(format "%s: %s" type msg)))
|
|
(type-symbol (if (string-equal "warning" type) :warning :error))
|
|
(src-pos (flymake-diag-region source-buffer row column)))
|
|
;; new Flymake diag message
|
|
(push (flymake-make-diagnostic source-buffer
|
|
(car src-pos)
|
|
;; buffer might have changed size
|
|
(min (buffer-size source-buffer) (cdr src-pos))
|
|
type-symbol
|
|
msg-text)
|
|
results)))
|
|
(forward-line 1))
|
|
results))))
|
|
|
|
;; heavily based on the example found at
|
|
;; https://www.gnu.org/software/emacs/manual/html_node/flymake/An-annotated-example-backend.html
|
|
(defun flymake-eslint--create-process (source-buffer callback)
|
|
"Internal function.
|
|
Create linter process for SOURCE-BUFFER which invokes CALLBACK once linter is finished. CALLBACK is passed one argument, which is a buffer containing stdout from linter."
|
|
(when (process-live-p flymake-eslint--process)
|
|
(kill-process flymake-eslint--process))
|
|
(let ((default-directory (or flymake-eslint-project-root default-directory)))
|
|
(setq flymake-eslint--process
|
|
(make-process
|
|
:name "flymake-eslint"
|
|
:connection-type 'pipe
|
|
:noquery t
|
|
:buffer (generate-new-buffer " *flymake-eslint*")
|
|
:command (list flymake-eslint-executable-name "--no-color" "--no-ignore" "--stdin" "--stdin-filename" (buffer-file-name source-buffer) (or flymake-eslint-executable-args ""))
|
|
:sentinel (lambda (proc &rest ignored)
|
|
;; do stuff upon child process termination
|
|
(when (and (eq 'exit (process-status proc))
|
|
;; make sure we're not using a deleted buffer
|
|
(buffer-live-p source-buffer)
|
|
;; make sure we're using the latest lint process
|
|
(with-current-buffer source-buffer (eq proc flymake-eslint--process)))
|
|
;; read from eslint output then destroy temp buffer when done
|
|
(let ((proc-buffer (process-buffer proc)))
|
|
(funcall callback proc-buffer)
|
|
(kill-buffer proc-buffer))))))))
|
|
|
|
(defun flymake-eslint--check-and-report (source-buffer flymake-report-fn)
|
|
"Internal function.
|
|
Run eslint against SOURCE-BUFFER and use FLYMAKE-REPORT-FN to report results."
|
|
(if flymake-eslint-defer-binary-check
|
|
(flymake-eslint--ensure-binary-exists))
|
|
(flymake-eslint--create-process
|
|
source-buffer
|
|
(lambda (eslint-stdout)
|
|
(funcall flymake-report-fn (flymake-eslint--report eslint-stdout source-buffer))))
|
|
(with-current-buffer source-buffer
|
|
(process-send-string flymake-eslint--process (buffer-string))
|
|
(process-send-eof flymake-eslint--process)))
|
|
|
|
(defun flymake-eslint--checker (flymake-report-fn &rest ignored)
|
|
"Internal function.
|
|
Run eslint on the current buffer, and report results using FLYMAKE-REPORT-FN. All other parameters are currently IGNORED."
|
|
(flymake-eslint--check-and-report (current-buffer) flymake-report-fn))
|
|
|
|
|
|
;; module entry point
|
|
|
|
|
|
;;;###autoload
|
|
(defun flymake-eslint-enable ()
|
|
"Enable Flymake and add flymake-eslint as a buffer-local Flymake backend."
|
|
(interactive)
|
|
(if (not flymake-eslint-defer-binary-check)
|
|
(flymake-eslint--ensure-binary-exists))
|
|
(make-local-variable 'flymake-eslint-project-root)
|
|
(flymake-mode t)
|
|
(add-hook 'flymake-diagnostic-functions 'flymake-eslint--checker nil t))
|
|
|
|
|
|
(provide 'flymake-eslint)
|
|
|
|
|
|
;;; flymake-eslint.el ends here
|