/ solidity-flycheck.el
solidity-flycheck.el
  1  ;;; solidity-flycheck.el --- Flycheck integration for solidity emacs mode
  2  
  3  ;; Copyright (C) 2015-2018  Lefteris Karapetsas
  4  
  5  ;; Author: Lefteris Karapetsas  <lefteris@refu.co>
  6  ;; Keywords: languages, solidity, flycheck
  7  ;; Version: 0.1.11
  8  ;; Package-Requires: ((flycheck "32-cvs") (solidity-mode "0.1.9") (dash "2.17.0"))
  9  
 10  ;; This program is free software; you can redistribute it and/or modify
 11  ;; it under the terms of the GNU General Public License as published by
 12  ;; the Free Software Foundation, either version 3 of the License, or
 13  ;; (at your option) any later version.
 14  
 15  ;; This program is distributed in the hope that it will be useful,
 16  ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
 17  ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 18  ;; GNU General Public License for more details.
 19  
 20  ;; You should have received a copy of the GNU General Public License
 21  ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
 22  
 23  ;;; Commentary:
 24  
 25  ;; flycheck integration is taken in a different file to prevent problems
 26  ;; when flycheck is not present on the user's system.
 27  ;;
 28  ;;; Code:
 29  
 30  (require 'flycheck)
 31  (require 'solidity-common)
 32  (require 'dash)
 33  (require 'project)
 34  
 35  (defcustom solidity-flycheck-chaining-error-level 'warning
 36    "The maximum error level at which chaining of checkers will happen.
 37  
 38  This means that this is the error level for which solc checker will allow
 39  next checkers to run.  By default this is the warning level.
 40  Possible values are:
 41  
 42  `info'
 43      If any errors higher than info level are found in solc, then solium
 44      will not run.
 45  
 46  `warning'
 47      If any errors higher than warning level are found in solc, then solium
 48      will not run.
 49  
 50  `error'
 51      If any errors higher than error level are found in solc, then solium
 52      will not run.
 53   t
 54      Solium will always run."
 55    :group 'solidity
 56    :type '(choice (const :tag "Chain after info-error level" info)
 57                   (const :tag "Chain after warning-error level" warning)
 58                   (const :tag "Chain after error-error level" error)
 59                   (const :tag "Always chain" t))
 60    :package-version '(solidity . "0.1.5")
 61    :safe #'symbolp)
 62  
 63  (flycheck-def-option-var flycheck-solidity-solium-soliumrcfile nil solium-check
 64    "The path to use for soliumrc.json
 65  
 66  The value of this variable is either a string denoting a path to the soliumrc.json
 67  or nil, to use the current directory.  When non-nil,
 68  we pass the directory to solium via the `--config' option."
 69    :type '(choice (const :tag "No custom soliumrc" nil)
 70                   (string :tag "Custom soliumrc file location"))
 71    :safe #'stringp
 72    :package-version '(solidity-mode . "0.1.4"))
 73  
 74  (defcustom solidity-flycheck-solc-additional-allow-paths nil
 75    "A list of paths that should be passed to solc using the --allow-paths option.
 76  
 77  This lets you import .sol files from other directories without causing linting errors.
 78  For example, say that you use Brownie (URL `https://github.com/eth-brownie/brownie'),
 79  which stores ethPM packages in \"~/.brownie/packages\". You could add \"~/.brownie/packages\"
 80  to this variable to import ethPM packages without linting errors. Subdirectories
 81  in each allow path are remapped.
 82  
 83  Note that when `solidity-flycheck-use-project' is t, the project root will be
 84  added to --allow-paths in addition to any paths defined here."
 85    :group 'solidity
 86    :type 'list
 87    :safe #'listp
 88    :package-version '(solidity-mode . "0.1.11"))
 89  
 90  (defcustom solidity-flycheck-use-project nil
 91    "A boolean flag denoting whether solidity-flycheck should detect projects.
 92  
 93  When t, solidity-flycheck will detect a project and include files in the project
 94  during compilation and linting. solidity-flycheck will look for a .soliumrc.json
 95  in a parent directory. If found, this directory is considered the project root. If
 96  no .soliumrc.json is found, `project-roots' is used.
 97  
 98  When using solium-checker, the .soliumrc.json found in the project root will be
 99  used as the solium config, rather than a .soliumrc.json in the same directory as
100  the file being linted.
101  
102  When using solhint-checker, solhint will be run at the project root.
103  
104  When using solidity-checker , the project root will be passed to solc using the
105  --allow-paths flag. This means imports to other files inside the project will
106  lint without erorr."
107    :group 'solidity
108    :type 'boolean
109    :safe #'booleanp
110    :package-version '(solidity-mode . "0.1.11"))
111  
112  (defun solidity-flycheck--find-working-directory (_checker)
113    "Look for a working directtory to run solium CHECKER in.
114  
115  This will be a parent directory that contains a `.soliumrc.json' file. If
116  no .soliumrc.json is found, `project-roots' is used."
117    (when (and buffer-file-name solidity-flycheck-use-project)
118      (when-let ((root (or
119                        (locate-dominating-file buffer-file-name ".soliumrc.json")
120                        (let* ((proj (project-current t))
121                               (roots (project-roots proj)))
122                          (car roots)))))
123        (expand-file-name root))))
124  
125    ;; define solium flycheck syntax checker
126    ;; expanded the flycheck-define-checker macro in order to eval certain args, as per advice given in gitter
127    ;; https://gitter.im/flycheck/flycheck?at=5a43b3a8232e79134d98872b
128    ;; first try to add solium to the checker's list since if we got solc
129    ;; it must come after it in the list due to it being chained after solc
130  (flycheck-def-executable-var solium-checker "solium")
131  
132  (defun solium-has-reporter ()
133    (let ((solium-full-path (funcall
134                             flycheck-executable-find
135                             (or flycheck-solium-checker-executable solidity-solium-path))))
136      (and solium-full-path
137           (string-match-p "--reporter" (shell-command-to-string (concat solium-full-path " --help"))))))
138  
139  (flycheck-define-command-checker 'solium-checker
140    "A Solidity linter using solium"
141    :command `(,solidity-solium-path
142               (eval (when (solium-has-reporter) "--reporter=gcc"))
143               (option "--config=" flycheck-solidity-solium-soliumrcfile concat)
144               "-f"
145               source-inplace)
146  :error-patterns `((error line-start (zero-or-more not-newline) "[Fatal error]" (message))
147                    (error line-start (zero-or-more " ") line ":" column (zero-or-more " ") "error" (message))
148                    (warning line-start (zero-or-more " ") line ":" column (zero-or-more " ") "warning" (message))
149                    ;; reporter=gcc formats
150                    (error line-start (file-name) ":" line ":" column ": error: " (message))
151                    (warning line-start (file-name) ":" line ":" column ": warning: " (message)))
152  
153    :error-filter
154    ;; Add fake line numbers if they are missing in the lint output
155    #'(lambda (errors)
156        (dolist (err errors)
157          (unless (flycheck-error-line err)
158            (setf (flycheck-error-line err) 1)))
159        errors)
160    :modes 'solidity-mode
161    :predicate #'(lambda nil (eq major-mode 'solidity-mode))
162    :next-checkers `((,solidity-flycheck-chaining-error-level . solhint-checker))
163    :standard-input 'nil
164    :working-directory 'solidity-flycheck--find-working-directory)
165  
166  ;; add a solidity mode callback to set the executable of solc for flycheck
167  ;; define solidity's flycheck syntax checker
168  ;; expanded the flycheck-define-checker macro in order to eval certain args, as per advice given in gitter
169  ;; https://gitter.im/flycheck/flycheck?at=5a43b3a8232e79134d98872b
170  (flycheck-def-executable-var solidity-checker "solc")
171  
172  (defun get-solc-version ()
173    "Query solc executable and return its version.
174  
175    The result is returned in a list with 3 elements.MAJOR MINOR PATCH.
176  
177   If the solc output can't be parsed an error is returned."
178    (let* ((solc-full-path (funcall
179                            flycheck-executable-find
180                            (or flycheck-solidity-checker-executable solidity-solc-path)))
181           (output (shell-command-to-string (format "%s --version" solc-full-path))))
182      (if (string-match "Version: \\([[:digit:]]+\\)\.\\([[:digit:]]+\\)\.\\([[:digit:]]+\\)" output)
183          (list (match-string 1 output)
184                (match-string 2 output)
185                (match-string 3 output))
186        (error "Could not parse the output of %s --version:\n %s" solc-full-path output))))
187  
188  (defun solc-gt-0.6.0 ()
189    "Return `t` if solc >= 0.6.0 and `nil` otherwise."
190    (let* ((version (get-solc-version))
191           (major (string-to-number (nth 0 version)))
192           (minor (string-to-number (nth 1 version)))
193           (patch (string-to-number (nth 2 version))))
194      (if (and (>= major 0) (>= minor 6)) t nil)))
195  
196  (defun solidity-flycheck--solc-allow-paths ()
197    (append
198     (mapcar #'expand-file-name solidity-flycheck-solc-additional-allow-paths)
199     '(".")))
200  
201  (defun solidity-flycheck--only-subdirectories (dir)
202    (-filter
203     (lambda (p)
204       (and (file-directory-p p)
205            (not (string-prefix-p "." (file-name-nondirectory p)))))
206     (directory-files dir t)))
207  
208  (defun solidity-flycheck--solc-remappings ()
209    (let* ((allow-paths (solidity-flycheck--solc-allow-paths)))
210      (->> allow-paths
211           (cl-remove-if-not #'file-exists-p)
212           (mapcar #'solidity-flycheck--only-subdirectories)
213           -flatten
214           (mapcar
215            (lambda (dir)
216              (let ((prefix (file-name-nondirectory dir)))
217                (concat prefix "=" dir)))))))
218  
219  (defun solidity-flycheck--solc-remappings-opt ()
220    (when-let ((remappings (solidity-flycheck--solc-remappings)))
221      remappings))
222  
223  (defun solidity-flycheck--solc-allow-paths-opt ()
224    (let ((allow-paths (mapconcat 'identity (solidity-flycheck--solc-allow-paths) ",")))
225      (when (not (string= "" allow-paths))
226        `("--allow-paths" ,allow-paths))))
227  
228  (defun solidity-flycheck--solc-cmd ()
229    `(,solidity-solc-path
230      (eval
231       (when (solc-gt-0.6.0)
232         `("--no-color"
233           ,@(solidity-flycheck--solc-allow-paths-opt)
234           ,@(solidity-flycheck--solc-remappings-opt))))
235      source-inplace))
236  
237  (flycheck-define-command-checker 'solidity-checker
238    "A Solidity syntax checker using the solc compiler"
239    :command (solidity-flycheck--solc-cmd)
240    :error-patterns '(
241                      ;; Solidity >= 0.6.0 error formats
242                      (error line-start "Error: " (message) "\n" (zero-or-more whitespace) "--> " (file-name) ":" line ":" column)
243                      (warning line-start "Warning: " (message) "\n" (zero-or-more whitespace) "--> " (file-name) ":" line ":" column)
244  
245                      ;; Solidity < 0.6.0 error formats
246                      (error line-start (file-name) ":" line ":" column ":" " Error: " (message))
247                      (error line-start (file-name) ":" line ":" column ":" " Compiler error: " (message))
248                      (error line-start "Error: " (message))
249                      (warning line-start (file-name) ":" line ":" column ":" " Warning: " (message)))
250    :modes 'solidity-mode
251    :predicate #'(lambda nil (eq major-mode 'solidity-mode))
252    :next-checkers
253    `((,solidity-flycheck-chaining-error-level . solium-checker)
254      (,solidity-flycheck-chaining-error-level . solhint-checker))
255    :standard-input 'nil
256    :working-directory 'solidity-flycheck--find-working-directory)
257  
258  (flycheck-def-executable-var solhint-checker "solhint")
259  (flycheck-define-command-checker 'solhint-checker
260    "A Solidity linter using solhint"
261    :command `(,solidity-solhint-path "-f" "unix" source-inplace)
262    :error-patterns `((error
263                       line-start (file-name) ":"  line ":" column ":" (zero-or-more " ")
264                       (zero-or-more " ") (message (one-or-more not-newline) "[Error/" (one-or-more not-newline) "]" ))
265                      (warning
266                       line-start (file-name) ":"  line ":" column ":" (zero-or-more " ")
267                       (zero-or-more " ") (message (one-or-more not-newline) "[Warning/" (one-or-more not-newline) "]" )))
268    :error-filter
269    ;; Add fake line numbers if they are missing in the lint output
270    #'(lambda (errors)
271        (dolist (err errors)
272          (unless (flycheck-error-line err)
273            (setf (flycheck-error-line err) 1)))
274        errors)
275    :modes 'solidity-mode
276    :predicate #'(lambda nil (eq major-mode 'solidity-mode))
277    :next-checkers 'nil
278    :standard-input 'nil
279    :working-directory 'solidity-flycheck--find-working-directory)
280  
281  (add-to-list 'flycheck-checkers 'solhint-checker)
282  (add-to-list 'flycheck-checkers 'solium-checker)
283  (add-to-list 'flycheck-checkers 'solidity-checker)
284  
285  (provide 'solidity-flycheck)
286  ;;; solidity-flycheck.el ends here