Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
… into release
  • Loading branch information
orzechowskid committed Apr 12, 2019
2 parents 7cc0990 + 194d510 commit 43e4cb1
Showing 1 changed file with 66 additions and 54 deletions.
120 changes: 66 additions & 54 deletions flymake-eslint.el
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
;;; flymake-eslint.el --- A Flymake backend for Javascript using eslint -*- lexical-binding: t; -*-

;;; Version: 1.1.0
;;; Version: 1.2.0

;;; Author: Dan Orzechowski

Expand Down Expand Up @@ -40,7 +40,7 @@
:group 'flymake-eslint)


(defcustom flymake-eslint-executable-args ""
(defcustom flymake-eslint-executable-args nil
"Extra arguments to pass to eslint."
:type 'string
:group 'flymake-eslint)
Expand All @@ -59,77 +59,88 @@ Name of the temporary file on which to run eslint.")
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 if `flymake-eslint-executable-name' can't be found on variable `exec-path'"
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--create-temp-file-from-buffer (source-buffer)
(defun flymake-eslint--report (eslint-stdout-buffer source-buffer)
"Internal function.
Create a temporary file containing contents of SOURCE-BUFFER, and return its name."
;; TODO: there's probably a better way to do all this
(with-current-buffer source-buffer
;; save the contents of `source-buffer' as a string
(let ((buffer-text (buffer-string))
(temp-file-name flymake-eslint--filename))
;; create the new temp file
(with-temp-file temp-file-name
(insert buffer-text))
;; return the name of the temp file
(identity temp-file-name))))


(defun flymake-eslint--report (source-buffer)
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 "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 (format "%s: %s [%s]" type msg lint-rule))
(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) (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 Flymake diag messages from contents of current buffer, as reported against SOURCE-BUFFER."
;; start at the top and check each line for an eslint message
(goto-char (point-min))
(if (looking-at "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 (format "%s: %s [%s]" type msg lint-rule))
(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) (cdr src-pos) type-symbol msg-text) results)))
(forward-line 1))
results)))

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))
(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-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 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."
(with-temp-buffer
;; eslint might report incorrect row/column numbers for unsaved buffers, so we
;; write the current buffer to a temp file and process that instead. Use a file
;; in the current directory, not the system's temp directory, in case .eslintrc or
;; other path-sensitive tools like babel are applied to this file
;; TODO I think that can be solved with --stdin and --stdin-filename ?
(let ((temp-file-name (flymake-eslint--create-temp-file-from-buffer source-buffer)))
(call-process flymake-eslint-executable-name nil t t "--no-ignore" "--no-color" temp-file-name)
(funcall flymake-report-fn (flymake-eslint--report source-buffer))
(delete-file temp-file-name))))

(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--ensure-binary-exists)
(flymake-eslint--check-and-report (current-buffer) flymake-report-fn))


Expand All @@ -140,6 +151,7 @@ Run eslint on the current buffer, and report results using FLYMAKE-REPORT-FN. A
(defun flymake-eslint-enable ()
"Enable Flymake and add flymake-eslint as a buffer-local Flymake backend."
(interactive)
(flymake-eslint--ensure-binary-exists)
(flymake-mode t)
(add-hook 'flymake-diagnostic-functions 'flymake-eslint--checker nil t))

Expand Down

0 comments on commit 43e4cb1

Please sign in to comment.