Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
doublep committed May 24, 2020
0 parents commit 34a3921
Show file tree
Hide file tree
Showing 12 changed files with 1,034 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Eldev-local
.eldev

*.elc

/dist
10 changes: 10 additions & 0 deletions Eldev
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
; -*- mode: emacs-lisp; lexical-binding: t; no-byte-compile: t -*-

(eldev-use-package-archive 'melpa-stable)

;; Flycheck 32 is not released yet, use snapshots.
(eldev-use-package-archive 'melpa-unstable)


;; Avoid including files in test "projects".
(setf eldev-standard-excludes (append eldev-standard-excludes '("./test/*/")))
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
:source-language: lisp
:uri-flycheck: https://www.flycheck.org/
:uri-eldev: https://github.com/doublep/eldev

= flycheck-eldev

Make {uri-flycheck}[Flycheck] use proper dependencies in
{uri-eldev}[Eldev] projects.

For a project to be detected, it must contain file `Eldev` or
`Eldev-local` in its root directory, even if Eldev doesn’t strictly
require that.

== Features

* No additional steps to be performed from the command line, not even
`eldev prepare`.

* Project dependencies are seen by Flycheck in Emacs. Similarly, if a
package is not declared as a dependency of your project, Flycheck
will complain about unimportable features or undeclared functions.

* Everything is done on-the-fly. As you edit your project’s
dependency list in its main `.el` file, added, removed or mistyped
dependency names immediately become available to Flycheck (there
might be some delays due to network, as Eldev needs to fetch them
first).

* Additional test dependencies (see `eldev-add-extra-dependencies`)
are seen from the test files, but not from the main files.
174 changes: 174 additions & 0 deletions flycheck-eldev.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
;;; flycheck-eldev.el --- Eldev support in Flycheck -*- lexical-binding: t -*-

;;; Copyright (C) 2020 Paul Pogonyshev

;; Author: Paul Pogonyshev <[email protected]>
;; Maintainer: Paul Pogonyshev <[email protected]>
;; Version: 0.9
;; Keywords: tools, convenience
;; Homepage: https://github.com/flycheck/flycheck-eldev
;; Package-Requires: ((flycheck "32") (dash "2.17") (emacs "24.4"))

;; This program 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 of
;; the License, or (at your option) any later version.

;; This program 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 this program. If not, see https://www.gnu.org/licenses.

;;; Commentary:

;; Add Eldev support to Flycheck.

;;; Code:

(require 'flycheck)
(require 'dash)


(defvar flycheck-eldev-active t
"Whether Eldev extension to Flycheck is active.")

(defvar flycheck-eldev-general-error
"Eldev cannot be initialized; check dependency declarations and file `Eldev'"
"Error shown when Eldev cannot be initialized.")

(defvar flycheck-eldev-incompatible-error
"Package `flycheck-eldev' is no longer compatible with Flycheck; please report a bug"
"Error shown if the extension is incompatible with future Flycheck.")


(defun flycheck-eldev-find-root (&optional from)
"Get Eldev project root or nil, if not inside one.
If FROM is nil, search from `default-directory'."
(-when-let (root (locate-dominating-file
(or from default-directory)
(lambda (dir) (or (file-exists-p (expand-file-name "Eldev" dir))
(file-exists-p (expand-file-name "Eldev-local" dir))))))
(expand-file-name root)))


(defun flycheck-eldev--compute-working-directory (original checker &rest etc)
"Use Eldev project root when checking Elisp.
If not inside an Eldev project or when `flycheck-eldev-active' is
nil, just call the original function."
(or (when (and flycheck-eldev-active (eq checker 'emacs-lisp))
(flycheck-eldev-find-root))
(apply original checker etc)))

(defun flycheck-eldev--start-command-checker (original checker callback &rest etc)
"Use Eldev instead of raw Emacs when appropriate."
(if (and flycheck-eldev-active (eq checker 'emacs-lisp) (flycheck-eldev-find-root))
(let* ((flycheck-emacs-lisp-load-path nil)
(flycheck-emacs-lisp-initialize-packages nil)
(original-wrapper-function flycheck-command-wrapper-function)
(flycheck-command-wrapper-function
(lambda (command)
(funcall original-wrapper-function
(flycheck-eldev--generate-command-line command)))))
(let* ((process (apply original checker callback etc))
(file-directory (file-name-directory (buffer-file-name))))
;; Hack: Eldev is run from the project root, but Emacs reports syntax errors
;; without a path. Therefore, we reset directory from the root to where the
;; file is actually contained after the process is started.
(process-put process 'flycheck-working-directory file-directory)
process))
(apply original checker callback etc)))

(defun flycheck-eldev--generate-command-line (command-line)
;; Although we heavily rewrite command line, it is still better to _rewrite_ rather than
;; completely replace it. E.g. we need the filename buried somewhere inside the form,
;; which `flycheck-substitute-argument' doesn't support. So, since we need rewriting
;; step anyway, let's take what native Elisp checker wants and massage it, so that we
;; get any future improvements for free (unless they break us, of course).
(or (let* ((head (cdr (-drop-last 2 command-line)))
(tail (-take-last 2 command-line))
(filename (cadr tail))
;; FIXME: Or is there a better way than getting it from the buffer?
(real-filename (buffer-file-name))
eval-forms)
(while head
(when (string= (pop head) "--eval")
(if (string-match-p (rx "(" (or "setq" "setf") " package-user-dir") (car head))
;; Just discard, Eldev will take care of this. Binding
;; `flycheck-emacs-lisp-package-user-dir' to nil would not be enough.
(pop head)
(push (pop head) eval-forms))))
;; Sanity check: fail if the standard checker wants something we don't expect.
(when (and (string= (car tail) "--")
(--any (string-match-p (rx "(byte-compile") it) eval-forms)
(--any (string-match-p (rx "command-line-args-left") it) eval-forms))
`(,(funcall flycheck-executable-find "eldev") "--color=never"
"--setup"
,(flycheck-sexp-to-string
`(advice-add #'eldev--package-dir-info :around
(lambda (original)
(eldev-advised
(#'insert-file-contents
:around
(lambda (original filename &rest arguments)
;; Ignore the original file for project initialization
;; purposes. If `eldev-project-main-file' is specified,
;; this does nothing.
(unless (file-equal-p filename ,real-filename)
;; Workaround, will probably go into Eldev itself:
;; `package-dir-info' chokes on unreadable files,
;; e.g. locks for buffers modified in Emacs.
(ignore-errors (apply original filename arguments)))))
(funcall original)))))
;; If Eldev cannot be initialized, report a fake error that can be seen in UI.
;; FIXME: If there is an error in file `Eldev' we won't get here...
"--setup"
,(flycheck-sexp-to-string
`(advice-add #'eldev-load-project-dependencies :around
(lambda (original &rest etc)
(condition-case error
(apply original etc)
;; E.g. mistyped dependency name.
(eldev-missing-dependency
(let ((message (cdr error)))
(while (keywordp (car message))
(setf message (cddr message)))
(eldev-print "%s:1:1: Error: %s"
,real-filename (apply #'eldev-format-message message)))
(signal 'eldev-quit 1))
;; Handle all other errors generically.
(error
(eldev-print ,(format "%s:1:1: Error: %s"
filename flycheck-eldev-general-error))
(signal 'eldev-quit 1))))))
;; When checking project's main file, use the temporary as the main file instead.
"--setup"
,(flycheck-sexp-to-string
`(when (and eldev-project-main-file (file-equal-p eldev-project-main-file ,real-filename))
(setf eldev-project-main-file ,filename)))
;; Special handling for test files: load extra dependencies as if testing now.
"--setup"
,(flycheck-sexp-to-string
`(when (eldev-filter-files '(,real-filename) eldev-test-fileset)
(dolist (dependency (cdr (assq 'test eldev--extra-dependencies)))
(eldev-add-extra-dependencies 'exec dependency))))
"exec" "--dont-require"
,(flycheck-sexp-to-string
`(setf command-line-args-left (list "--" ,filename)))
,@(nreverse eval-forms))))
;; Fallback in case we fail outright.
`(,(funcall flycheck-executable-find "emacs") "--batch" "--eval"
,(flycheck-sexp-to-string
`(message "setup:1:1: Error: %s" ,flycheck-eldev-incompatible-error)))))


;; We don't really define anything new, just hack what Flycheck already provides a bit.
(advice-add #'flycheck-compute-working-directory :around #'flycheck-eldev--compute-working-directory)
(advice-add #'flycheck-start-command-checker :around #'flycheck-eldev--start-command-checker)


(provide 'flycheck-eldev)

;;; flycheck-eldev.el ends here
83 changes: 83 additions & 0 deletions test/flycheck-eldev-test.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
(require 'flycheck-eldev)
(require 'ert)
(require 'dash)


(defvar flycheck-eldev--test-dir (file-name-directory (or load-file-name (buffer-file-name))))


(defmacro flycheck-eldev--test (file &rest body)
(declare (indent 1) (debug (sexp body)))
;; Don't use `emacs-lisp-checkdoc'.
`(let ((flycheck-checkers '(emacs-lisp))
(flycheck-disabled-checkers nil)
(flycheck-check-syntax-automatically nil)
(file (expand-file-name ,file flycheck-eldev--test-dir)))
(with-temp-buffer
(insert-file-contents file t)
(setf default-directory (file-name-directory file))
(emacs-lisp-mode)
(flycheck-mode 1)
,@body)))

(defun flycheck-eldev--test-recheck ()
(flycheck-buffer)
(should (eq flycheck-last-status-change 'running))
(let ((start-time (float-time)))
(while (eq flycheck-last-status-change 'running)
;; Be generous, what if Eldev needs to install dependencies?
(when (> (- (float-time) start-time) 30.0)
(ert-fail "timed out"))
(accept-process-output nil 0.02))
(should (eq flycheck-last-status-change 'finished))))

(defun flycheck-eldev--test-expect-no-errors ()
;; Only exists for a better name.
(flycheck-eldev--test-expect-errors))

(defun flycheck-eldev--test-expect-errors (&rest errors)
(let ((actual-errors flycheck-current-errors)
(expected-errors errors))
(while (or actual-errors expected-errors)
(let ((actual (pop actual-errors))
(expected (pop expected-errors)))
(cond ((and actual expected)
(-when-let (regexp (plist-get expected :matches))
(unless (string-match-p regexp (flycheck-error-message actual))
(ert-fail (format-message "unexpected error %S: expected message matching '%s'" actual regexp)))))
(actual
(ert-fail (format-message "unexpected error: %S" actual)))
(expected
(ert-fail (format-message "expected error not detected: %S" expected))))))))

(ert-deftest flycheck-eldev-basics-1 ()
(flycheck-eldev--test "project-a/project-a.el"
(flycheck-eldev--test-recheck)
(flycheck-eldev--test-expect-no-errors)))

(ert-deftest flycheck-eldev-test-file-1 ()
(flycheck-eldev--test "project-a/test/project-a.el"
(flycheck-eldev--test-recheck)
(flycheck-eldev--test-expect-no-errors)))

(ert-deftest flycheck-eldev-deactivating-1 ()
(let ((flycheck-eldev-active nil))
(flycheck-eldev--test "project-a/project-a.el"
(flycheck-eldev--test-recheck)
(flycheck-eldev--test-expect-errors '(:matches "dependency-a")))))

;; Test that removing dependencies gives errors immediately.
(ert-deftest flycheck-eldev-remove-dependency-1 ()
(flycheck-eldev--test "project-a/project-a.el"
(search-forward "(dependency-a \"1.0\")")
(replace-match "")
(flycheck-eldev--test-recheck)
(flycheck-eldev--test-expect-errors '(:matches "dependency-a"))))

;; Test that adding unaccessible dependencies gives errors immediately.
(ert-deftest flycheck-eldev-add-dependency-1 ()
(flycheck-eldev--test "project-a/project-a.el"
(search-forward "(dependency-a \"1.0\")")
(insert " (some-totally-bulshit-dependency)")
(flycheck-eldev--test-recheck)
(flycheck-eldev--test-expect-errors '(:matches "not available"))))
3 changes: 3 additions & 0 deletions test/package-archive-a/archive-contents
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(1
(dependency-a . [(1 0) nil "Dependency test package A" single nil])
(dependency-b . [(1 0) ((dependency-a (0))) "Dependency test package B" single nil]))
10 changes: 10 additions & 0 deletions test/package-archive-a/dependency-a-1.0.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
;;; dependency-a.el --- Dependency test package A

;; Version: 1.0

(defun dependency-a-hello ()
"Hello")

(provide 'dependency-a)

;;; dependency-a.el ends here
14 changes: 14 additions & 0 deletions test/package-archive-a/dependency-b-1.0.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
;;; dependency-b.el --- Dependency test package B

;; Version: 1.0
;; Package-Requires: (dependency-a)


(require 'dependency-a)

(defun dependency-b-hello ()
(dependency-a-hello))

(provide 'dependency-b)

;;; dependency-b.el ends here
3 changes: 3 additions & 0 deletions test/project-a/Eldev
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; -*- mode: emacs-lisp; lexical-binding: t; no-byte-compile: t -*-

(eldev-use-package-archive `("archive-a" . ,(expand-file-name "../package-archive-a")))
22 changes: 22 additions & 0 deletions test/project-a/project-a.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
;;; project-a.el --- Test project with one dependency

;; Version: 1.0
;; Homepage: https://example.com/
;; Package-Requires: ((dependency-a "1.0"))

;;; Commentary:

;; Comments to make linters happy.

;;; Code:

(require 'dependency-a)

(defun project-a-hello ()
"Invoke `dependency-a-hello'.
This docstring exists to make linters happy."
(dependency-a-hello))

(provide 'project-a)

;;; project-a.el ends here
5 changes: 5 additions & 0 deletions test/project-a/test/project-a.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(require 'project-a)
(require 'ert)

(ert-deftest project-a-test-hello ()
(should (string= (project-a-hello) "Hello")))

0 comments on commit 34a3921

Please sign in to comment.