aboutsummaryrefslogtreecommitdiff
path: root/elisp/pylint.el
blob: 327da0fcbef6784a263e2d1649d28cb4464cc21d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
;;; pylint.el --- minor mode for running `pylint'

;; Copyright (c) 2009, 2010 Ian Eure <ian.eure@gmail.com>
;; Author: Ian Eure <ian.eure@gmail.com>
;; Maintainer: Jonathan Kotta <jpkotta@gmail.com>

;; Keywords: languages python
;; Version: 1.02

;; pylint.el 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 2, or (at your option) any later
;; version.
;;
;; It 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 your copy of 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:
;;
;; Specialized compile mode for pylint.  You may want to add the
;; following to your init.el:
;;
;;   (autoload 'pylint "pylint")
;;   (add-hook 'python-mode-hook 'pylint-add-menu-items)
;;   (add-hook 'python-mode-hook 'pylint-add-key-bindings)
;;
;; There is also a handy command `pylint-insert-ignore-comment' that
;; makes it easy to insert comments of the form `# pylint:
;; ignore=msg1,msg2,...'.

;;; Code:

(require 'compile)
(require 'tramp)

(defgroup pylint nil
  "Minor mode for running the Pylint Python checker"
  :prefix "pylint-"
  :group 'tools
  :group 'languages)

(defvar pylint-last-buffer nil
  "The most recent PYLINT buffer.
A PYLINT buffer becomes most recent when you select PYLINT mode in it.
Notice that using \\[next-error] or \\[compile-goto-error] modifies
`completion-last-buffer' rather than `pylint-last-buffer'.")

(defconst pylint-regexp-alist
  (let ((base "^\\(.*\\):\\([0-9]+\\):\s+\\(\\[%s.*\\)$"))
    (list
     (list (format base "[FE]") 1 2)
     (list (format base "[RWC]") 1 2 nil 1)))
  "Regexp used to match PYLINT hits.  See `compilation-error-regexp-alist'.")

(defcustom pylint-options '("--reports=n" "--output-format=parseable")
  "Options to pass to pylint.py"
  :type '(repeat string)
  :group 'pylint)

(defcustom pylint-use-python-indent-offset nil
  "If non-nil, use `python-indent-offset' to set indent-string."
  :type 'boolean
  :group 'pylint)

(defcustom pylint-command "pylint"
  "PYLINT command."
  :type '(file)
  :group 'pylint)

(defcustom pylint-alternate-pylint-command "pylint2"
  "Command for pylint when invoked with C-u."
  :type '(file)
  :group 'pylint)

(defcustom pylint-ask-about-save nil
  "Non-nil means \\[pylint] asks which buffers to save before compiling.
Otherwise, it saves all modified buffers without asking."
  :type 'boolean
  :group 'pylint)

(defvar pylint--messages-list ()
  "A list of strings of all pylint messages.")

(defvar pylint--messages-list-hist ()
  "Completion history for `pylint--messages-list'.")

(defun pylint--sort-messages (a b)
  "Compare function for sorting `pylint--messages-list'.

Sorts most recently used elements first using `pylint--messages-list-hist'."
  (let ((idx 0)
        (a-idx most-positive-fixnum)
        (b-idx most-positive-fixnum))
    (dolist (e pylint--messages-list-hist)
      (when (string= e a)
        (setq a-idx idx))
      (when (string= e b)
        (setq b-idx idx))
      (setq idx (1+ idx)))
    (< a-idx b-idx)))

(defun pylint--create-messages-list ()
  "Use `pylint-command' to populate `pylint--messages-list'."
  ;; example output:
  ;;  |--we want this--|
  ;;  v                v
  ;; :using-cmp-argument (W1640): *Using the cmp argument for list.sort / sorted*
  ;;   Using the cmp argument for list.sort or the sorted builtin should be avoided,
  ;;   since it was removed in Python 3. Using either `key` or `functools.cmp_to_key`
  ;;   should be preferred. This message can't be emitted when using Python >= 3.0.
  (setq pylint--messages-list
        (split-string
         (with-temp-buffer
           (shell-command (concat pylint-command " --list-msgs") (current-buffer))
           (flush-lines "^[^:]")
           (goto-char (point-min))
           (while (not (eobp))
             (delete-char 1) ;; delete ";"
             (re-search-forward " ")
             (delete-region (point) (line-end-position))
             (forward-line 1))
           (buffer-substring-no-properties (point-min) (point-max))))))

;;;###autoload
(defun pylint-insert-ignore-comment (&optional arg)
  "Insert a comment like \"# pylint: disable=msg1,msg2,...\".

This command repeatedly uses `completing-read' to match known
messages, and ultimately inserts a comma-separated list of all
the selected messages.

With prefix argument, only insert a comma-separated list (for
appending to an existing list)."
  (interactive "*P")
  (unless pylint--messages-list
    (pylint--create-messages-list))
  (setq pylint--messages-list
        (sort pylint--messages-list #'pylint--sort-messages))
  (let ((msgs ())
        (msg "")
        (prefix (if arg
                    ","
                  "# pylint: disable="))
        (sentinel "[DONE]"))
    (while (progn
             (setq msg (completing-read
                        "Message: "
                        pylint--messages-list
                        nil t nil 'pylint--messages-list-hist sentinel))
             (unless (string= sentinel msg)
               (add-to-list 'msgs msg 'append))))
    (setq pylint--messages-list-hist
          (delete sentinel pylint--messages-list-hist))
    (insert prefix (mapconcat 'identity msgs ","))))

(define-compilation-mode pylint-mode "PYLINT"
  (setq pylint-last-buffer (current-buffer))
  (set (make-local-variable 'compilation-error-regexp-alist)
       pylint-regexp-alist)
  (set (make-local-variable 'compilation-disable-input) t))

(defvar pylint-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map compilation-minor-mode-map)
    (define-key map " " 'scroll-up)
    (define-key map "\^?" 'scroll-down)
    (define-key map "\C-c\C-f" 'next-error-follow-minor-mode)

    (define-key map "\r" 'compile-goto-error)  ;; ?
    (define-key map "n" 'next-error-no-select)
    (define-key map "p" 'previous-error-no-select)
    (define-key map "{" 'compilation-previous-file)
    (define-key map "}" 'compilation-next-file)
    (define-key map "\t" 'compilation-next-error)
    (define-key map [backtab] 'compilation-previous-error)
    map)
  "Keymap for PYLINT buffers.
`compilation-minor-mode-map' is a cdr of this.")

(defun pylint--make-indent-string ()
  "Make a string for the `--indent-string' option."
  (format "--indent-string='%s'"
          (make-string python-indent-offset ?\ )))

;;;###autoload
(defun pylint (&optional arg)
  "Run PYLINT, and collect output in a buffer, much like `compile'.

While pylint runs asynchronously, you can use \\[next-error] (M-x next-error),
or \\<pylint-mode-map>\\[compile-goto-error] in the grep \
output buffer, to go to the lines where pylint found matches.

\\{pylint-mode-map}"
  (interactive "P")

  (save-some-buffers (not pylint-ask-about-save) nil)
  (let* ((filename (buffer-file-name))
         (localname-offset (cl-struct-slot-offset 'tramp-file-name 'localname))
         (filename (or (and (tramp-tramp-file-p filename)
                         (elt (tramp-dissect-file-name filename) localname-offset))
                      filename))
         (filename (shell-quote-argument filename))
         (pylint-command (if arg
                             pylint-alternate-pylint-command
                           pylint-command))
         (pylint-options (if (not pylint-use-python-indent-offset)
                             pylint-options
                           (append pylint-options
                                   (list (pylint--make-indent-string)))))
         (command (mapconcat
                   'identity
                   (append `(,pylint-command) pylint-options `(,filename))
                   " ")))

    (compilation-start command 'pylint-mode)))

;;;###autoload
(defun pylint-add-key-bindings ()
  (let ((map (cond
              ((boundp 'py-mode-map) py-mode-map)
              ((boundp 'python-mode-map) python-mode-map))))

    ;; shortcuts in the tradition of python-mode and ropemacs
    (define-key map (kbd "C-c m l") 'pylint)
    (define-key map (kbd "C-c m p") 'previous-error)
    (define-key map (kbd "C-c m n") 'next-error)
    (define-key map (kbd "C-c m i") 'pylint-insert-ignore-comment)
    nil))

;;;###autoload
(defun pylint-add-menu-items ()
  (let ((map (cond
              ((boundp 'py-mode-map) py-mode-map)
              ((boundp 'python-mode-map) python-mode-map))))

    (define-key map [menu-bar Python pylint-separator]
      '("--" . pylint-separator))
    (define-key map [menu-bar Python next-error]
      '("Next error" . next-error))
    (define-key map [menu-bar Python prev-error]
      '("Previous error" . previous-error))
    (define-key map [menu-bar Python lint]
      '("Pylint" . pylint))
    nil))

(provide 'pylint)

;;; pylint.el ends here