Klimi's new dotfiles with stow.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

254 lines
10 KiB

;;; ess-r-flymake.el --- A ess-r Flymake backend -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2018 J. Alexander Branham (alex DOT branham AT gmail DOT com)
;; Copyright (C) 2018 ESS-core team
;; Maintainer: ESS-core <ESS-core@r-project.org>
;;
;; This file is NOT part of GNU Emacs.
;;
;; This is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free
;; Software Foundation; either version 3, or (at your option) any later
;; version.
;;
;; This is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
;; for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
;; MA 02110-1301 USA.
;;
;;; Commentary:
;;
;; Flymake is the built-in Emacs package that supports on-the-fly
;; syntax checking. This file adds support for this in ess-r-mode by
;; relying on the lintr package, available on CRAN and currently
;; hosted at https://github.com/jimhester/lintr.
;;
;; It is enabled by default.
;;
;;; Code:
(eval-when-compile (require 'cl-lib))
(require 'ess-inf)
(require 'flymake)
;; Appease the byte compiler for Emacs 25. Remove after dropping
;; support for Emacs 25.
(declare-function flymake-diag-region "flymake")
(declare-function flymake-make-diagnostic "flymake")
(declare-function flymake--overlays "flymake")
(defcustom ess-r-flymake-linters
'("closed_curly_linter = NULL"
"commas_linter = NULL"
"commented_code_linter = NULL"
"infix_spaces_linter = NULL"
"line_length_linter = NULL"
"object_length_linter = NULL"
"object_name_linter = NULL"
"object_usage_linter = NULL"
"open_curly_linter = NULL"
"pipe_continuation_linter = NULL"
"single_quotes_linter = NULL"
"spaces_inside_linter = NULL"
"spaces_left_parentheses_linter = NULL"
"trailing_blank_lines_linter = NULL"
"trailing_whitespace_linter = NULL")
"Default linters to use.
Can be either a string with R expression to be used as
is (e.g. 'lintr::default_linters'). Or a list of strings where
each element is passed as argument to 'lintr::with_defaults'."
:group 'ess-R
:type '(choice string (repeat string))
:package-version '(ess . "18.10"))
(defcustom ess-r-flymake-lintr-cache t
"If non-nil, cache lintr results."
:group 'ess-R
:type 'boolean
:package-version '(ess . "18.10"))
(defvar-local ess-r--flymake-proc nil)
(defvar-local ess-r--lintr-file nil
"Location of the .lintr file for this buffer.")
(defvar ess-r--flymake-def-linter
(replace-regexp-in-string
"[\n\t ]+" " "
"esslint <- function(str, ...) {
if (!suppressWarnings(require(lintr, quietly=T))) {
cat('@@error: @@`lintr` package not installed')
} else {
if (packageVersion('lintr') <= '1.0.3') {
cat('@@error: @@Need `lintr` version > v1.0.3')
} else {
tryCatch(lintr::lint(commandArgs(TRUE), ...),
error = function(e) {
cat('@@warning: @@', e)
})
}
}
};"))
(defun ess-r--find-lintr-file ()
"Return the absolute path to the .lintr file.
Check first the current directory, then the project root, then
the user's home directory. Return nil if we couldn't find a .lintr file."
(let ((cur-dir-file (expand-file-name ".lintr" default-directory))
(ess-proj-file (and (fboundp 'ess-r-package-project)
(ess-r-package-project)
(expand-file-name ".lintr" (cdr (ess-r-package-project)))))
(proj-file (and (project-current)
(project-roots (project-current))
(expand-file-name ".lintr" (car (project-roots (project-current))))))
(home-file (expand-file-name ".lintr" (getenv "HOME"))))
(cond (;; current directory
(file-readable-p cur-dir-file)
cur-dir-file)
;; Project root according to `ess-r-package-project'
((and ess-proj-file
(file-readable-p ess-proj-file))
ess-proj-file)
;; Project root according to `project-roots'
((and proj-file
(file-readable-p proj-file)))
;; Home directory
((file-readable-p home-file)
home-file))))
(defun ess-r--flymake-linters ()
"If `ess-r-flymake-linters' is a string, use that.
Otherwise, construct a string to pass to lintr::with_defaults."
(replace-regexp-in-string
"[\n\t ]+" " "
(if (stringp ess-r-flymake-linters)
ess-r-flymake-linters
(concat "lintr::with_defaults("
(mapconcat #'identity
ess-r-flymake-linters
", ")
")"))))
(defun ess-r--flymake-msg-type (str)
"Transform STR into log level."
(cond ((string= str "error: ") :error)
((string= str "warning: ") :warning)
((string= str "style: ") :note)
(t (error "Invalid msg type %s" str))))
(defun ess-r--flymake-check-errors ()
"Check for critical errors and return non-nil if such occurred."
(goto-char (point-min))
(when (re-search-forward "@@\\(\\(error\\|warning\\): \\)@@" nil t)
(let ((type (ess-r--flymake-msg-type (match-string 1)))
(msg (buffer-substring-no-properties (match-end 0) (point-max))))
(flymake-log type msg)
(eq type :error))))
(defun ess-r--flymake-parse-output (msg-buffer src-buffer report-fn)
"Parse the content of MSG-BUFFER for lint locations.
SRC-BUFFER is the original source buffer. Collect all messages
into a list and call REPORT-FN on it."
(with-current-buffer msg-buffer
(if (ess-r--flymake-check-errors)
(with-current-buffer src-buffer
;; we are in the sentinel here; don't throw but remove our hook instead
(remove-hook 'flymake-diagnostic-functions 'ess-r-flymake t))
(goto-char (point-min))
(cl-loop
while (search-forward-regexp
;; Regex to match the output lint() gives us.
(rx line-start "<text>:"
;; row
(group-n 1 (one-or-more num)) ":"
;; column
(group-n 2 (one-or-more num)) ": "
;; type
(group-n 3 (| "style: " "warning: " "error: "))
;; msg
(group-n 4 (one-or-more not-newline)) line-end)
nil t)
for msg = (match-string 4)
for (beg . end) = (let ((line (string-to-number (match-string 1)))
(col (string-to-number (match-string 2))))
(flymake-diag-region src-buffer line col))
for type = (ess-r--flymake-msg-type (match-string 3))
collect (flymake-make-diagnostic src-buffer beg end type msg)
into diags
finally (funcall report-fn diags)))))
(defun ess-r-flymake (report-fn &rest _args)
"A Flymake backend for ESS-R modes. Relies on the lintr package.
REPORT-FN is flymake's callback function."
(unless (executable-find inferior-ess-r-program)
(error "Cannot find program '%s'" inferior-ess-r-program))
;; Kill the process if earlier check was found. The sentinel of the earlier
;; check will detect this.
(when (process-live-p ess-r--flymake-proc)
(kill-process ess-r--flymake-proc))
(if (and (eql ess-use-flymake 'process)
(not (ess-process-live-p)))
(progn
(funcall report-fn nil)
(mapc #'delete-overlay (flymake--overlays)))
(let ((src-buffer (current-buffer)))
(setq ess-r--flymake-proc
(make-process
:name "ess-r-flymake" :noquery t :connection-type 'pipe
:buffer (generate-new-buffer " *ess-r-flymake*")
:command (list inferior-R-program
"--no-save" "--no-restore" "--no-site-file" "--no-init-file" "--slave"
"-e" (concat
(when ess-r--lintr-file
(concat "options(lintr.linter_file = \"" ess-r--lintr-file "\");"))
ess-r--flymake-def-linter
;; commandArgs(TRUE) returns everything after
;; --args as a character vector
"esslint(commandArgs(TRUE)"
(unless ess-r--lintr-file
(concat ", linters = " (ess-r--flymake-linters)))
(when ess-r-flymake-lintr-cache
", cache = TRUE")
")")
"--args" (buffer-substring-no-properties
(point-min) (point-max)))
:sentinel
(lambda (proc _event)
(cond
((eq 'exit (process-status proc))
(unwind-protect
(if (eq proc (buffer-local-value 'ess-r--flymake-proc src-buffer))
(ess-r--flymake-parse-output (process-buffer proc) src-buffer report-fn)
(flymake-log :warning "Canceling obsolete check %s" proc))
(kill-buffer (process-buffer proc))))
((not (eq 'run (process-status proc)))
(kill-buffer (process-buffer proc))))))))))
(defun ess-r-setup-flymake ()
"Setup flymake for ESS.
Activate flymake only if `ess-use-flymake' is non-nil."
(when ess-use-flymake
(when (< emacs-major-version 26)
(error "ESS-flymake requires Emacs version 26 or later"))
(when (string= "R" ess-dialect)
(setq ess-r--lintr-file (ess-r--find-lintr-file))
(add-hook 'flymake-diagnostic-functions #'ess-r-flymake nil t)
;; Try not to enable flymake if flycheck is already running:
(unless (bound-and-true-p flycheck-mode)
(flymake-mode)))))
;; Enable flymake in Emacs 26+
(when (<= 26 emacs-major-version)
(if (eval-when-compile (<= 26 emacs-major-version))
(add-hook 'ess-r-mode-hook #'ess-r-setup-flymake)
(when ess-use-flymake
(display-warning 'ess "ESS was compiled with older version of Emacs;\n `ess-r-flymake' won't be available"))))
(provide 'ess-r-flymake)
;;; ess-r-flymake.el ends here