├── .gitignore ├── .emacs ├── .gitignore ├── dependencies.el └── init.el ├── .dir-locals.el ├── test ├── test-elisp-lint.el ├── undercover-init.el └── data │ └── example.el ├── Makefile ├── .circleci └── config.yml ├── README.md └── elisp-lint.el /.gitignore: -------------------------------------------------------------------------------- 1 | .elpa 2 | *.elc 3 | elisp-lint-autoloads.el 4 | -------------------------------------------------------------------------------- /.emacs/.gitignore: -------------------------------------------------------------------------------- 1 | .emacs-custom.el 2 | network-security.data 3 | elpa/ 4 | quelpa/ 5 | 6 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((emacs-lisp-mode . ((indent-tabs-mode . nil) 2 | (fill-column . 80)))) 3 | -------------------------------------------------------------------------------- /test/test-elisp-lint.el: -------------------------------------------------------------------------------- 1 | ;;; test-elisp-lint.el --- unit tests 2 | 3 | (load-file "test/undercover-init.el") 4 | (require 'elisp-lint) 5 | 6 | (describe "sample test" 7 | (it "does nothing" 8 | (expect 1 :to-equal 1))) 9 | 10 | ;;; test-elisp-lint.el ends here 11 | -------------------------------------------------------------------------------- /test/undercover-init.el: -------------------------------------------------------------------------------- 1 | ;;; undercover-init.el --- setup undercover 2 | 3 | ;; `load-file` this into any other file containing tests 4 | 5 | (when (require 'undercover nil t) 6 | (undercover "*.el" 7 | (:report-file "coverage.json") 8 | (:send-report nil))) 9 | 10 | ;;; undercover-init.el ends here 11 | -------------------------------------------------------------------------------- /.emacs/dependencies.el: -------------------------------------------------------------------------------- 1 | ;;; dependencies.el - project specific package dependencies 2 | 3 | (use-package buttercup 4 | :ensure t) 5 | 6 | (use-package undercover 7 | :ensure t) 8 | 9 | (use-package let-alist 10 | :pin gnu ; version 1.0.6 on ELPA 11 | :ensure t) 12 | 13 | (use-package package-lint 14 | :ensure t) 15 | 16 | (use-package dash 17 | :ensure t) 18 | 19 | ;;; dependencies.el ends here 20 | -------------------------------------------------------------------------------- /test/data/example.el: -------------------------------------------------------------------------------- 1 | ;;; d.el --- Vestibulum convallis lorem a tempus semper dui euismod elit -*- lexical-binding:t -*- 2 | 3 | ;; Version: 1 4 | 5 | (declare-function diff "diff") 6 | (declare-function foobar "diff") 7 | 8 | (let ((a x) 9 | (b y)) 10 | (message "%s" a)) 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | (setq a 3) 13 | 14 | ;; this is a comment. single space emacs. 15 | 16 | ;; Package-Requires: ((a "123") (b "345") (a "123") (b "345") (a "123") (b "345") (a "123") (b "345")) 17 | 18 | (defun foo () 19 | "emacs is fun" 20 | ) 21 | 22 | (auth-source-pick-first-password :user "foo" :host "bar") 23 | 24 | (not-a-function 'boo) 25 | 26 | (provide 'difftest) 27 | ;;; difftest ends here 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export EMACS ?= emacs 2 | export BATCH = --batch -q -l .emacs/init.el 3 | 4 | ELS = $(filter-out elisp-lint-autoloads.el,$(wildcard *.el)) 5 | TESTS = $(wildcard test/*.el) 6 | OBJECTS = $(ELS:.el=.elc) 7 | BACKUPS = $(ELS:.el=.el~) $(TESTS:.el=.el~) 8 | 9 | .PHONY: version lint test clean cleanelpa 10 | 11 | .elpa: 12 | mkdir -p .emacs/elpa/gnupg && \ 13 | chmod 700 .emacs/elpa/gnupg && \ 14 | echo "disable-ipv6" > .emacs/elpa/gnupg/dirmngr.conf && \ 15 | for i in {1..3}; do \ 16 | gpg --keyserver keyserver.ubuntu.com \ 17 | --homedir .emacs/elpa/gnupg \ 18 | --recv-keys 066DAFCB81E42C40 \ 19 | && break || sleep 15; \ 20 | done 21 | $(EMACS) $(BATCH) 22 | touch .elpa 23 | 24 | version: .elpa 25 | $(EMACS) $(BATCH) --version 26 | 27 | lint: .elpa 28 | $(EMACS) $(BATCH) -l elisp-lint.el -f elisp-lint-files-batch $(ELS) 29 | $(EMACS) $(BATCH) -l elisp-lint.el -f elisp-lint-files-batch \ 30 | --no-byte-compile \ 31 | --no-package-lint \ 32 | --no-checkdoc \ 33 | --no-check-declare $(TESTS) 34 | 35 | test: .elpa 36 | $(EMACS) $(BATCH) -f buttercup-run-discover 37 | 38 | coverage.json: .elpa $(ELS) $(TESTS) 39 | UNDERCOVER_FORCE=1 $(EMACS) $(BATCH) -f buttercup-run-discover 40 | 41 | submit-coverage: coverage.json 42 | curl -s https://codecov.io/bash | bash -s - -f coverage.json 43 | 44 | clean: 45 | rm -f $(OBJECTS) $(BACKUPS) elisp-lint-autoloads.el* coverage.json 46 | 47 | cleanelpa: clean 48 | rm -rf .emacs/elpa .emacs/quelpa .emacs/.emacs-custom.el* .elpa 49 | 50 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # Default actions to perform on each Emacs version 4 | commands: 5 | default-steps: 6 | steps: 7 | - checkout 8 | - run: make version 9 | - run: make lint 10 | - run: make test 11 | 12 | # Enumerated list of Emacs versions 13 | jobs: 14 | test-emacs-24: 15 | docker: 16 | - image: silex/emacs:24-ci-cask 17 | entrypoint: bash 18 | steps: 19 | - default-steps 20 | 21 | test-emacs-25: 22 | docker: 23 | - image: silex/emacs:25-ci-cask 24 | entrypoint: bash 25 | steps: 26 | - default-steps 27 | 28 | test-emacs-26: 29 | docker: 30 | - image: silex/emacs:26-ci-cask 31 | entrypoint: bash 32 | steps: 33 | - default-steps 34 | 35 | test-emacs-27: 36 | docker: 37 | - image: silex/emacs:27-ci-cask 38 | entrypoint: bash 39 | steps: 40 | - default-steps 41 | 42 | test-emacs-master: 43 | docker: 44 | - image: silex/emacs:master-ci-cask 45 | entrypoint: bash 46 | steps: 47 | - default-steps 48 | 49 | coverage: 50 | docker: 51 | - image: silex/emacs:26-ci-cask 52 | entrypoint: bash 53 | steps: 54 | - checkout 55 | - run: make clean submit-coverage 56 | 57 | # Executing in parallel 58 | workflows: 59 | ci-test-matrix: 60 | jobs: 61 | - test-emacs-24 62 | - test-emacs-25 63 | - test-emacs-26 64 | - test-emacs-27 65 | - test-emacs-master 66 | - coverage: 67 | requires: 68 | - test-emacs-24 69 | - test-emacs-25 70 | - test-emacs-26 71 | - test-emacs-27 72 | - test-emacs-master 73 | -------------------------------------------------------------------------------- /.emacs/init.el: -------------------------------------------------------------------------------- 1 | ;;; init.el - Emacs initialization for isolated package testing 2 | ;; 3 | ;; Usage: emacs -q -l $project_root/emacs/init.el 4 | 5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 6 | ;; Set `user-emacs-directory' to avoid overwriting $HOME/.emacs.d 7 | ;; See also: https://debbugs.gnu.org/cgi/bugreport.cgi?bug=15539#66 8 | 9 | (setq user-init-file (or load-file-name (buffer-file-name))) 10 | (setq user-emacs-directory (file-name-directory user-init-file)) 11 | 12 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 13 | ;; Make sure customize data doesn't land in this file 14 | 15 | (setq custom-file (concat user-emacs-directory ".emacs-custom.el")) 16 | (when (file-readable-p custom-file) (load custom-file)) 17 | 18 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 19 | ;; Set load-path to include the .el files for the project under development, 20 | ;; which reside in the parent of `user-emacs-directory'. Adding this path 21 | ;; permits tests to require this package itself. 22 | (add-to-list 'load-path (expand-file-name ".." user-emacs-directory)) 23 | 24 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 25 | ;; Configure melpa and melpa-stable 26 | 27 | (require 'package) 28 | (add-to-list 'package-archives 29 | '("melpa-stable" . "https://stable.melpa.org/packages/") t) 30 | (add-to-list 'package-archives 31 | '("melpa" . "https://melpa.org/packages/") t) 32 | (setq package-enable-at-startup nil) 33 | (package-initialize) 34 | (when (not package-archive-contents) 35 | (package-refresh-contents)) 36 | 37 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 38 | ;; Bootstrap `use-package' 39 | 40 | ;; By default these will install from melpa anyway, but using 41 | ;; `package-pinned-packages' allows one to pin to melpa-stable 42 | ;; if necessary 43 | 44 | (setq package-pinned-packages 45 | '((bind-key . "melpa") 46 | (diminish . "melpa") 47 | (use-package . "melpa"))) 48 | 49 | (dolist (p (mapcar 'car package-pinned-packages)) 50 | (unless (package-installed-p p) 51 | (package-install p))) 52 | 53 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 54 | ;; Install `quelpa' and `quelpa-use-package' 55 | 56 | (use-package quelpa 57 | ;; :pin melpa-stable 58 | :ensure t) 59 | 60 | (use-package quelpa-use-package 61 | ;; :pin melpa-stable 62 | :ensure t) 63 | 64 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 65 | ;; Load project dependencies from elsewhere 66 | 67 | (load (concat user-emacs-directory "dependencies.el")) 68 | 69 | ;;; init.el ends here 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | elisp-lint 2 | ========== 3 | 4 | Basic linting for Emacs Lisp 5 | 6 | [![MELPA Stable](https://stable.melpa.org/packages/elisp-lint-badge.svg)](https://stable.melpa.org/#/elisp-lint) 7 | [![MELPA](https://melpa.org/packages/elisp-lint-badge.svg)](https://melpa.org/#/elisp-lint) 8 | [![CircleCI](https://img.shields.io/circleci/project/github/gonewest818/elisp-lint.svg)](https://circleci.com/gh/gonewest818/elisp-lint) 9 | [![codecov](https://codecov.io/gh/gonewest818/elisp-lint/branch/master/graph/badge.svg)](https://codecov.io/gh/gonewest818/elisp-lint) 10 | 11 | This is a tool for finding certain problems in Emacs Lisp files. Use it on the command line like this: 12 | 13 | emacs -Q --batch -l elisp-lint.el -f elisp-lint-files-batch *.el 14 | 15 | You can disable individual checks by passing flags on the command line: 16 | 17 | emacs -Q --batch -l elisp-lint.el -f elisp-lint-files-batch --no-indent *.el 18 | 19 | You can use file variables or `.dir-locals.el` to disable checks completely, and 20 | also to configure certain checks as described below. 21 | 22 | ((emacs-lisp-mode . ((fill-column . 80) 23 | (indent-tabs-mode . nil) 24 | (elisp-lint-ignored-validators . ("byte-compile")) 25 | (elisp-lint-indent-specs . ((describe . 1) 26 | (it . 1)))))) 27 | 28 | Validators 29 | ---------- 30 | 31 | ### byte-compile ### 32 | 33 | Byte-compiles the file with all warnings enabled. 34 | 35 | ### check-declare ### 36 | 37 | Verifies all `declare-function` statements are legitimate. 38 | 39 | ### checkdoc ### 40 | 41 | Runs checkdoc on the file to enforce standards in documentation. 42 | 43 | ### fill-column ### 44 | 45 | Verifies that no line exceeds the number of columns in `fill-column`. 46 | Certain lines in the file are excluded from this check, and can 47 | have unlimited length: 48 | 49 | * The package summary comment line, which by definition must 50 | include the package name, a summary description (up to 60 51 | characters), and an optional `-*- lexical-binding:t -*-` 52 | declaration. 53 | 54 | * The \"Package-Requires\" header, whose length is determined by 55 | the number of dependencies specified. 56 | 57 | ### indent ### 58 | 59 | Verifies that each line is indented according to 60 | `emacs-lisp-mode`. Where macros are defined with special `indent` 61 | metadata, use the `elisp-lint-indent-specs` alist to specify each 62 | symbol's required indent. 63 | 64 | ### indent-character ### 65 | 66 | Verifies the indentation is consistently tabs or spaces, according to 67 | the value of `indent-tabs-mode`. 68 | 69 | ### package-lint ### 70 | 71 | Use `package-lint` to perform checks on package metadata and elisp 72 | style. 73 | 74 | Because this check uses the `package` library to validate when 75 | dependencies can be installed from an available package repository, 76 | this check will suppress any "package is not installable" errors when 77 | running in a context where the `package` library is not initialized. 78 | This can happen, e.g. when running Emacs on packages installed with 79 | Cask. 80 | 81 | ### trailing-whitespace ### 82 | 83 | Verifies the buffer has no lines with trailing whitespace. 84 | 85 | Configuration 86 | ------------- 87 | 88 | Use a file variable or `.dir-locals.el` to override the variables 89 | mentioned above. 90 | 91 | Sample Report 92 | ------------- 93 | 94 | ``` text 95 | test/data/example.el:0:0 (checkdoc) You should have a section marked ";;; Commentary:" 96 | test/data/example.el:0:0 (checkdoc) The first line should be of the form: ";;; package --- Summary" 97 | test/data/example.el:1:0 (error) Package should have a ;;; Commentary section. 98 | test/data/example.el:1:0 (error) Package should have a Homepage or URL header. 99 | test/data/example.el:1:0 (error) package.el cannot parse this buffer: Package lacks a file header 100 | test/data/example.el:5:0 (checkdoc) You should have a section marked ";;; Code:" 101 | test/data/example.el:8:0 (checkdoc) White space found at end of line 102 | test/data/example.el:8:0 (whitespace) trailing whitespace found 103 | test/data/example.el:9:0 (indent) ! (b y)) 104 | test/data/example.el:10:0 (indent) ! (message "%s" a)) 105 | test/data/example.el:10:0 (indent-character) tabs instead of spaces 106 | test/data/example.el:10:0 (whitespace) trailing whitespace found 107 | test/data/example.el:11:0 (fill-column) line length 80 exceeded 108 | test/data/example.el:12:0 (indent) ! (setq a 3) 109 | test/data/example.el:16:0 (error) "foo" doesn't start with package's prefix "difftest". 110 | test/data/example.el:17:0 (checkdoc) First line should be capitalized 111 | test/data/example.el:17:0 (checkdoc) First sentence should end with punctuation 112 | test/data/example.el:17:0 (indent) ! "emacs is fun" 113 | test/data/example.el:18:0 (warning) Closing parens should not be wrapped onto new lines. 114 | test/data/example.el:18:0 (indent) ! ) 115 | test/data/example.el:21:0 (checkdoc) The footer should be: (provide 'example)\n;;; example.el ends here 116 | test/data/example.el FAIL 117 | ``` 118 | 119 | Changelog 120 | --------- 121 | 122 | * Version 0.5-SNAPSHOT (MELPA) 123 | - suppress "Package X is not installable" errors when running in 124 | a context where 'package-initialize' hasn't occurred 125 | * Version 0.4-SNAPSHOT (MELPA Stable, March 2020) 126 | - Provide a summary report of all tests [#20] 127 | - Integrate package-lint [#19] 128 | - Remove package-format, as package-lint covers the same territory 129 | - Make byte-compile errors and warnings more robust 130 | - Make the fill-column checker ignore the package summary line [#25] 131 | - Make 'fill-column' checker ignore the package requires header 132 | - Add dependency on dash.el 133 | - Colorized output 134 | * Version 0.3 (December 2019) 135 | - Emacs 23 support is deprecated [#13] 136 | - Adopt CircleCI and drop Travis CI [#9] [#14] 137 | - Add check-declare validator [#16] 138 | - Generate autoloads before bytecompile [#8] 139 | * Version 0.2.0 (Feb 2018) 140 | - Project transferred to new maintainer 141 | - Whitespace check permits page-delimiter (^L) 142 | - Indentation check prints the diff to console 143 | - User can specify indent specs to tell the checker about macros 144 | - Added checkdoc (available only Emacs 25 and newer) 145 | - Cleared up the console output for easier reading in CI 146 | - Expand Travis CI test matrix to include Emacs 25 and 26 147 | * Version 0.1.0 (2015) 148 | - Basic linting functionality implemented 149 | 150 | Credits 151 | ------- 152 | 153 | The initial development of `elisp-lint` is Copyright 2013-2015 Nikolaj 154 | Schumacher. This project was transferred to Neil Okamoto in 2018. 155 | 156 | Updates and ongoing development are Copyright 2018-2020 Neil Okamoto and contributors. 157 | 158 | Contributing 159 | ------------ 160 | 161 | Pull requests are welcome! 162 | -------------------------------------------------------------------------------- /elisp-lint.el: -------------------------------------------------------------------------------- 1 | ;;; elisp-lint.el --- Basic linting for Emacs Lisp -*- lexical-binding:t -*- 2 | ;; 3 | ;; Copyright (C) 2013-2015 Nikolaj Schumacher 4 | ;; Copyright (C) 2018-2022 Neil Okamoto 5 | ;; 6 | ;; Author: Nikolaj Schumacher , 7 | ;; Maintainer: Neil Okamoto 8 | ;; Version: 0.5.0-SNAPSHOT 9 | ;; Keywords: lisp, maint, tools 10 | ;; Package-Requires: ((emacs "24.4") (dash "2.15.0") (package-lint "0.11")) 11 | ;; URL: http://github.com/gonewest818/elisp-lint/ 12 | ;; 13 | ;; This file is NOT part of GNU Emacs. 14 | ;; 15 | ;; This program is free software; you can redistribute it and/or 16 | ;; modify it under the terms of the GNU General Public License 17 | ;; as published by the Free Software Foundation; either version 2 18 | ;; of the License, or (at your option) any later version. 19 | ;; 20 | ;; This program is distributed in the hope that it will be useful, 21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | ;; GNU General Public License for more details. 24 | ;; 25 | ;; You should have received a copy of the GNU General Public License 26 | ;; along with this program. If not, see . 27 | ;; 28 | ;;; Commentary: 29 | ;; 30 | ;; This is a tool for finding certain problems in Emacs Lisp files. Use it on 31 | ;; the command line like this: 32 | ;; 33 | ;; $(EMACS) -Q --batch -l elisp-lint.el -f elisp-lint-files-batch *.el 34 | ;; 35 | ;; You can disable individual checks by passing flags on the command line: 36 | ;; 37 | ;; $(EMACS) -Q --batch -l elisp-lint.el -f elisp-lint-files-batch \ 38 | ;; --no-indent *.el 39 | ;; 40 | ;; Alternatively, you can disable checks using file variables or the following 41 | ;; .dir-locals.el file: 42 | ;; 43 | ;; ((emacs-lisp-mode . ((elisp-lint-ignored-validators . ("fill-column"))))) 44 | ;; 45 | ;; For a full list of validators, see 'elisp-lint-file-validators' and 46 | ;; 'elisp-lint-buffer-validators'. 47 | ;; 48 | ;;; Change Log: 49 | ;; 50 | ;; * Version 0.5-SNAPSHOT (MELPA) 51 | ;; - suppress "Package X is not installable" errors when running in 52 | ;; a context where 'package-initialize' hasn't occurred 53 | ;; - Make 'fill-column' checker ignore URLs in comments or strings [#29] 54 | ;; * Version 0.4 (MELPA Stable, March 2020) 55 | ;; - Provide a summary report of all tests [#20] 56 | ;; - Integrate 'package-lint' [#19] 57 | ;; - Remove 'package-format', as 'package-lint' covers the same territory 58 | ;; - Make byte-compile errors and warnings more robust 59 | ;; - Make 'fill-column' checker ignore the package summary line [#25] 60 | ;; - Make 'fill-column' checker ignore the package requires header 61 | ;; - Add dependency on 'dash.el' 62 | ;; - Colorized output 63 | ;; * Version 0.3 (December 2019) 64 | ;; - Emacs 23 support is deprecated [#13] 65 | ;; - Adopt CircleCI and drop Travis CI [#9] [#14] 66 | ;; - Add 'check-declare' validator [#16] 67 | ;; - Generate autoloads before byte-compile [#8] 68 | ;; * Version 0.2 (Feb 2018) 69 | ;; - Project transferred to new maintainer 70 | ;; - Whitespace check permits page-delimiter (^L) 71 | ;; - Indentation check prints the diff to console 72 | ;; - User can specify indent specs to tell the checker about macros 73 | ;; - Added 'checkdoc' (available only Emacs 25 and newer) 74 | ;; - Cleared up the console output for easier reading in CI 75 | ;; - Expand Travis CI test matrix to include Emacs 25 and 26 76 | ;; * Version 0.1 (2015) 77 | ;; - Basic linting functionality implemented 78 | ;; 79 | ;;; Code: 80 | 81 | (require 'bytecomp) 82 | (require 'check-declare) 83 | (require 'checkdoc nil t) 84 | (require 'package) 85 | (require 'package-lint) 86 | (require 'subr-x) 87 | (require 'dash) 88 | 89 | (defconst elisp-lint-file-validators 90 | '("byte-compile" 91 | "check-declare")) 92 | 93 | (defconst elisp-lint-buffer-validators 94 | (append (when (fboundp 'checkdoc-current-buffer) 95 | '("checkdoc")) 96 | '("package-lint" 97 | "indent" 98 | "indent-character" 99 | "fill-column" 100 | "trailing-whitespace"))) 101 | 102 | (defvar elisp-lint-ignored-validators nil 103 | "List of validators that should not be run.") 104 | (put 'elisp-lint-ignored-validators 'safe-local-variable 'listp) 105 | 106 | (defvar elisp-lint-batch-files nil 107 | "List of files to be processed in batch execution.") 108 | 109 | (defvar elisp-lint-indent-specs nil 110 | "Alist of symbols and their indent specifiers. 111 | The property 'lisp-indent-function will be set accordingly on 112 | each of the provided symbols prior to running the indentation 113 | check. Caller can set this variable as needed on the command 114 | line or in \".dir-locals.el\". The alist should take the form 115 | `((symbol1 . spec1) (symbol2 . spec2) ...)' where the specs are 116 | identical to the `indent' declarations in defmacro.") 117 | (put 'elisp-lint-indent-specs 'safe-local-variable 'listp) 118 | 119 | (defvar elisp-lint--debug nil 120 | "Toggle when debugging interactively for extra warnings, etc.") 121 | 122 | (defmacro elisp-lint--protect (&rest body) 123 | "Handle errors raised in BODY." 124 | (declare (indent 0) (debug t)) 125 | `(condition-case err 126 | (progn ,@body) 127 | (error (message "%s" (error-message-string err)) nil))) 128 | 129 | (defmacro elisp-lint--run (validator &rest args) 130 | "Run the VALIDATOR with ARGS." 131 | `(unless (member ,validator elisp-lint-ignored-validators) 132 | (let ((v (elisp-lint--protect 133 | (funcall (intern (concat "elisp-lint--" ,validator)) ,@args)))) 134 | (copy-tree v)))) ;; TODO: is deep copy necessary? 135 | 136 | (defun elisp-lint--handle-argv () 137 | "Parse command line and find flags to disable specific validators. 138 | Push results to `elisp-lint-ignored-validators' and `elisp-lint-batch-files'." 139 | (dolist (option command-line-args-left) 140 | (cond ((string-match "^--no-\\([a-z-]*\\)" option) 141 | (add-to-list 'elisp-lint-ignored-validators 142 | (substring-no-properties option 5))) 143 | (t (add-to-list 'elisp-lint-batch-files option)))) 144 | (setq command-line-args-left nil)) ; empty this. we've handled all. 145 | 146 | ;;; Validators 147 | 148 | (defvar elisp-lint--autoloads-filename nil 149 | "The autoloads file for this package.") 150 | 151 | (defun elisp-lint--generate-autoloads () 152 | "Generate autoloads and set `elisp-lint--autoloads-filename'. 153 | Assume `default-directory' name is also the package name, 154 | e.g. for this package it will be \"elisp-lint-autoloads.el\"." 155 | (let* ((dir (directory-file-name default-directory)) 156 | (prefix (file-name-nondirectory dir)) 157 | (pkg (intern prefix)) 158 | (load-prefer-newer t) 159 | (inhibit-message t)) 160 | (package-generate-autoloads pkg dir) 161 | (setq elisp-lint--autoloads-filename (format "%s-autoloads.el" prefix)))) 162 | 163 | (defun elisp-lint--byte-compile (path-to-file) 164 | "Byte-compile PATH-TO-FILE with warnings enabled. 165 | Return a list of errors, or nil if none found." 166 | (let ((comp-log "*Compile-Log*") 167 | (lines nil) 168 | (byte-compile-warnings t) 169 | (file (file-name-nondirectory path-to-file))) 170 | (unless elisp-lint--autoloads-filename 171 | (elisp-lint--generate-autoloads)) 172 | (let ((inhibit-message t)) 173 | (load-file elisp-lint--autoloads-filename)) 174 | (when (get-buffer comp-log) (kill-buffer comp-log)) 175 | (byte-compile-file path-to-file) 176 | ;; Using `get-buffer-create' to avoid a message if compilation hasn't 177 | ;; produced any warnings and thus created the buffer. 178 | (with-current-buffer (get-buffer-create comp-log) 179 | (goto-char (point-min)) 180 | (while (not (eobp)) 181 | (if (looking-at file) 182 | (let* ((end-pos (save-excursion ; continuation on next line? 183 | (beginning-of-line 2) 184 | (if (looking-at " ") 2 1))) 185 | (item (split-string 186 | (buffer-substring-no-properties 187 | (line-beginning-position) 188 | (line-end-position end-pos)) 189 | ":"))) 190 | (push (list (string-to-number (nth 1 item)) ; LINE 191 | (string-to-number (nth 2 item)) ; COL 192 | 'byte-compile ; TYPE 193 | (string-trim ; MSG 194 | (mapconcat #'identity (cdddr item) ":"))) 195 | lines))) 196 | (beginning-of-line 2))) 197 | lines)) 198 | 199 | (defun elisp-lint--check-declare (file) 200 | "Validate `declare-function' statements in FILE." 201 | (let ((errlist (check-declare-file file))) 202 | (mapcar 203 | (lambda (item) 204 | ;; check-declare-file returns a list of items containing, from 205 | ;; left to right, the name of the library where 'declare-function' 206 | ;; said to find the definition, followed by a list of the filename 207 | ;; we are currently linting, the function name being looked up, 208 | ;; and the error returned by 'check-declare-file': 209 | ;; 210 | ;; ((".../path/to/library1.el.gz" ("foo.el" "func1" "err message")) 211 | ;; (".../path/to/library2.el.gz" ("foo.el" "func2" "err message")) 212 | ;; ... 213 | ;; (".../path/to/libraryN.el.gz" ("foo.el" "funcN" "err message"))) 214 | ;; 215 | ;; For now we don't get line numbers for warnings, but the 216 | ;; 'declare-function' lines are easy for the user to find. 217 | (list 0 0 'check-declare 218 | (format "(declare-function) %s: \"%s\" in file \"%s\"" 219 | (car (cddadr item)) 220 | (cadadr item) 221 | (car item)))) 222 | errlist))) 223 | 224 | ;; Checkdoc is available only Emacs 25 or newer 225 | (when (fboundp 'checkdoc-current-buffer) 226 | (defun elisp-lint--checkdoc () 227 | "Run checkdoc on the current buffer. 228 | Parse warnings and return in a list, or nil if no errors found." 229 | (let ((style-buf "*Style Warnings*") 230 | (lines nil)) 231 | (when (get-buffer style-buf) (kill-buffer style-buf)) 232 | (checkdoc-current-buffer t) 233 | (with-current-buffer style-buf 234 | (goto-char (point-min)) 235 | (beginning-of-line 5) ; skip empty lines and ^L 236 | (while (not (eobp)) 237 | (let ((item (split-string 238 | (buffer-substring-no-properties 239 | (line-beginning-position) (line-end-position)) 240 | ":"))) 241 | (push (list (string-to-number (nth 1 item)) ; LINE 242 | 0 ; COLUMN 243 | 'checkdoc ; TYPE 244 | (string-trim 245 | (mapconcat #'identity (cddr item) ":"))) ; MSG 246 | lines) 247 | (beginning-of-line 2)))) 248 | lines))) 249 | 250 | (defun elisp-lint--package-lint () 251 | "Run package-lint on buffer and return results. 252 | Result is a list of one item per line having an error, and each 253 | entry contains: (LINE COLUMN TYPE MESSAGE) 254 | 255 | Because package-lint uses the package library to validate when 256 | dependencies can be installed, this function checks for when the 257 | package library has NOT been initialized, and suppresses the 258 | inevitable \"not installable\" errors in that case." 259 | (let ((err (-map 260 | (lambda (item) 261 | (-update-at 2 262 | (lambda (s) 263 | (make-symbol (concat "package-lint:" 264 | (symbol-name s)))) 265 | item)) 266 | (package-lint-buffer)))) 267 | (if package-archive-contents ; if package.el is initialized? 268 | err ; return the errors 269 | (-remove ; else remove "not installable" 270 | (lambda (item) 271 | (string-match "^Package [^ ]+ is not installable." (nth 3 item))) 272 | err)))) 273 | 274 | (defun elisp-lint--next-diff () 275 | "Search via regexp for the next diff in the current buffer. 276 | We expect this buffer to contain the output of \"diff -C 0\" and 277 | that the point is advancing through the buffer as it is parsed. 278 | Here we know each diff entry will be formatted like this if the 279 | indentation problem occurs in an isolated line: 280 | 281 | *************** 282 | *** 195 **** 283 | ! (let ((tick (buffer-modified-tick))) 284 | --- 195 ---- 285 | ! (let ((tick (buffer-modified-tick))) 286 | 287 | or formatted like this if there is a series of lines: 288 | 289 | *************** 290 | *** 195,196 **** 291 | ! (let ((tick (buffer-modified-tick))) 292 | ! (indent-region (point-min) (point-max)) 293 | --- 195,196 ---- 294 | ! (let ((tick (buffer-modified-tick))) 295 | ! (indent-region (point-min) (point-max)) 296 | 297 | So we will search for the asterisks and line numbers. Return a 298 | list containing the range of line numbers for this next 299 | diff. Return nil if no more diffs found in the buffer." 300 | (when (re-search-forward 301 | "^\\*\\*\\* \\([0-9]+\\),*\\([0-9]*\\) \\*\\*\\*\\*$" nil t) 302 | (let* ((r1 (string-to-number (match-string-no-properties 1))) 303 | (r2 (match-string-no-properties 2)) 304 | (r2 (if (equal r2 "") r1 (string-to-number r2)))) 305 | (beginning-of-line 2) ; leave point at start of next line 306 | (number-sequence r1 r2)))) 307 | 308 | (defun elisp-lint--indent () 309 | "Confirm buffer indentation is consistent with `emacs-lisp-mode'. 310 | Use `indent-region' to format the entire buffer, and compare the 311 | results to the filesystem. Return a list of diffs if there are 312 | any discrepancies. Prior to indenting the buffer, apply the 313 | settings provided in `elisp-lint-indent-specs' to configure 314 | specific symbols (typically macros) that require special 315 | handling. Result is a list of one item per line having an error, 316 | and each entry contains: (LINE COLUMN TYPE MESSAGE)" 317 | (dolist (s elisp-lint-indent-specs) 318 | (put (car s) 'lisp-indent-function (cdr s))) 319 | (let ((tick (buffer-modified-tick)) 320 | (errlist nil)) 321 | (let ((inhibit-message t)) 322 | (indent-region (point-min) (point-max))) 323 | (unless (equal tick (buffer-modified-tick)) 324 | (let ((diff-switches "-C 0")) (diff-buffer-with-file)) 325 | (revert-buffer t t) ; revert indent changes 326 | (with-current-buffer "*Diff*" 327 | (goto-char (point-min)) 328 | (while (not (eobp)) 329 | (let ((line-range (elisp-lint--next-diff))) 330 | (if line-range 331 | (mapc (lambda (linenum) ; loop over the range and report 332 | (push (list linenum 0 'indent 333 | (buffer-substring-no-properties 334 | (line-beginning-position) 335 | (line-end-position))) errlist) 336 | (beginning-of-line 2)) ; next line 337 | line-range) 338 | (goto-char (point-max))))) 339 | (kill-buffer))) 340 | errlist)) 341 | 342 | (defun elisp-lint--indent-character () 343 | "Verify buffer indentation is consistent with `indent-tabs-mode'. 344 | Use a file variable or \".dir-locals.el\" to override the default value." 345 | (let ((lines nil) 346 | (re (if indent-tabs-mode 347 | (elisp-lint--not-tab-regular-expression) 348 | "^\t")) 349 | (msg (if indent-tabs-mode 350 | "spaces instead of tabs" 351 | "tabs instead of spaces"))) 352 | (save-excursion 353 | (goto-char (point-min)) 354 | (while (re-search-forward re nil t) 355 | (push (list (count-lines (point-min) (point)) 356 | 0 'indent-character msg) lines))) 357 | lines)) 358 | 359 | (defun elisp-lint--not-tab-regular-expression () 360 | "Regex to match a string of spaces with a length of `tab-width'." 361 | (concat "^" (make-string tab-width ? ))) 362 | 363 | (defvar elisp-lint--package-summary-regexp 364 | "^;;; \\([^ ]*\\)\\.el ---[ \t]*\\(.*?\\)[ \t]*\\(-\\*-.*-\\*-[ \t]*\\)?$" 365 | "This regexp must match the definition in package.el.") 366 | 367 | (defvar elisp-lint--package-requires-regexp 368 | "^;;[ \t]+Package-Requires:" 369 | "This regexp must match the definition in package.el.") 370 | 371 | (defvar elisp-lint--url-in-document-regexp 372 | (concat "^" 373 | "[[:blank:]]*" 374 | "\\(?:;+\\|\"\\)?" 375 | "[[:blank:]]*" 376 | "[a-zA-Z][a-zA-Z0-9\-+.]*://" 377 | "[][;,/?:@&=+$_.!~*'()#%[:alnum:]-]+" 378 | "[[:blank:]]*\"?[[:blank:]]*" 379 | "[[:blank:]]*)*[[:blank:]]*" 380 | "$") 381 | "This regexp must match a URL in comments or strings.") 382 | 383 | (defun elisp-lint--fill-column () 384 | "Confirm buffer has no lines exceeding `fill-column' in length. 385 | Use a file variable or \".dir-locals.el\" to override the default 386 | value. 387 | 388 | Certain lines in the file are excluded from this check, and can 389 | have unlimited length: 390 | 391 | * The package summary comment line, which by definition must 392 | include the package name, a summary description (up to 60 393 | characters), and an optional \"-*- lexical-binding:t -*-\" 394 | declaration. 395 | 396 | * The \"Package-Requires\" header, whose length is determined by 397 | the number of dependencies specified. 398 | 399 | * Long URLs in comments or strings." 400 | (save-excursion 401 | (let ((line-number 1) 402 | (too-long-lines nil)) 403 | (goto-char (point-min)) 404 | (while (not (eobp)) 405 | (let ((text (buffer-substring-no-properties 406 | (line-beginning-position) 407 | (line-end-position)))) 408 | (when 409 | (and (not (string-match elisp-lint--package-summary-regexp text)) 410 | (not (string-match elisp-lint--package-requires-regexp text)) 411 | (not (string-match elisp-lint--url-in-document-regexp text)) 412 | (> (length text) fill-column)) 413 | (push (list line-number 0 'fill-column 414 | (format "line length %s exceeded" fill-column)) 415 | too-long-lines))) 416 | (setq line-number (1+ line-number)) 417 | (forward-line 1)) 418 | too-long-lines))) 419 | 420 | (defun elisp-lint--trailing-whitespace () 421 | "Confirm buffer has no line with trailing whitespace. 422 | Allow `page-delimiter' if it is alone on a line." 423 | (save-excursion 424 | (let ((lines nil)) 425 | (goto-char (point-min)) 426 | (while (re-search-forward "[[:space:]]+$" nil t) 427 | (unless (string-match-p 428 | (concat page-delimiter "$") ; allow a solo page-delimiter 429 | (buffer-substring-no-properties (line-beginning-position) 430 | (line-end-position))) 431 | (push (list (count-lines (point-min) (point)) 0 432 | 'whitespace "trailing whitespace found") 433 | lines))) 434 | lines))) 435 | 436 | ;;; Colorized output 437 | 438 | ;; Derived from similar functionality in buttercup.el 439 | ;; whose implementation is also licensed under the GPL: 440 | ;; https://github.com/jorgenschaefer/emacs-buttercup/ 441 | 442 | (defconst elisp-lint--ansi-colors 443 | '((black . 30) 444 | (red . 31) 445 | (green . 32) 446 | (yellow . 33) 447 | (blue . 34) 448 | (magenta . 35) 449 | (cyan . 36) 450 | (white . 37)) 451 | "ANSI color escape codes.") 452 | 453 | (defconst elisp-lint--no-color 454 | (let ((term (getenv-internal "TERM")) 455 | (no-color (getenv-internal "NO_COLOR"))) 456 | (or (and (stringp term) (string= term "dumb")) 457 | (and (stringp no-color) (> (length no-color) 0)))) 458 | "Disable colored text via the environment: NO_COLOR non-empty OR TERM=dumb.") 459 | 460 | (defun elisp-lint--print (color fmt &rest args) 461 | "Print output text in COLOR, formatted according to FMT and ARGS." 462 | (if elisp-lint--no-color 463 | (progn 464 | (princ (apply #'format fmt args)) 465 | (terpri)) 466 | (let ((ansi-val (cdr (assoc color elisp-lint--ansi-colors))) 467 | (cfmt (concat "\u001b[%sm" fmt "\u001b[0m"))) 468 | (princ (apply #'format cfmt ansi-val args)) 469 | (terpri)))) 470 | 471 | ;;; Linting 472 | 473 | (defun elisp-lint-file (file) 474 | "Run validators on FILE." 475 | (with-temp-buffer 476 | (find-file file) 477 | (let ((warnings (-concat (-mapcat (lambda (validator) 478 | (elisp-lint--run validator file)) 479 | elisp-lint-file-validators) 480 | (-mapcat (lambda (validator) 481 | (elisp-lint--run validator)) 482 | elisp-lint-buffer-validators)))) 483 | (mapc (lambda (w) 484 | ;; TODO: with two passes we could exactly calculate the number of 485 | ;; spaces to indent after the filenames and line numbers. 486 | (elisp-lint--print 'cyan "%-32s %s" 487 | (format "%s:%d:%d (%s)" 488 | file (nth 0 w) (nth 1 w) (nth 2 w)) 489 | (nth 3 w))) 490 | (sort warnings (lambda (x y) (< (car x) (car y))))) 491 | (not warnings)))) 492 | 493 | (defun elisp-lint-files-batch () 494 | "Run validators on all files specified on the command line." 495 | (elisp-lint--handle-argv) 496 | (when elisp-lint--debug 497 | (elisp-lint--print 'cyan "files: %s" 498 | elisp-lint-batch-files) 499 | (elisp-lint--print 'cyan "ignored: %s" 500 | elisp-lint-ignored-validators) 501 | (elisp-lint--print 'cyan "file validators: %s" 502 | elisp-lint-file-validators) 503 | (elisp-lint--print 'cyan "buffer validators: %s" 504 | elisp-lint-buffer-validators)) 505 | (let ((success t)) 506 | (dolist (file elisp-lint-batch-files) 507 | (if (elisp-lint-file file) 508 | (elisp-lint--print 'green "%s OK" file) 509 | (elisp-lint--print 'red "%s FAIL" file) 510 | (setq success nil))) 511 | (unless elisp-lint--debug (kill-emacs (if success 0 1))))) 512 | 513 | ;; ELISP> 514 | ;; (let ((command-line-args-left '("--no-byte-compile" 515 | ;; "--no-package-format" 516 | ;; "--no-checkdoc" 517 | ;; "--no-check-declare" 518 | ;; "example.el"))) 519 | ;; (setq elisp-lint-ignored-validators nil 520 | ;; elisp-lint-file-validators nil 521 | ;; elisp-lint-buffer-validators nil 522 | ;; elisp-lint-batch-files nil) 523 | ;; (elisp-lint-files-batch)) 524 | 525 | (provide 'elisp-lint) 526 | 527 | ;;; elisp-lint.el ends here 528 | --------------------------------------------------------------------------------