[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/relint 2e0bb22 09/10: Keep track of local variable bind
From: |
Mattias Engdegård |
Subject: |
[elpa] externals/relint 2e0bb22 09/10: Keep track of local variable bindings |
Date: |
Sun, 4 Aug 2019 13:42:48 -0400 (EDT) |
branch: externals/relint
commit 2e0bb226fee67c18fb13dc5df4f6794f5b932f05
Author: Mattias Engdegård <address@hidden>
Commit: Mattias Engdegård <address@hidden>
Keep track of local variable bindings
Simplified local variable tracking during the phase-2 traversal: every
let-bound variable either has an accurate value or is a known unknown.
setq on a local variable makes it unknown, since phase-2 does not
track program flow.
---
relint.el | 450 ++++++++++++++++++++++++++++++++++++++------------------------
1 file changed, 277 insertions(+), 173 deletions(-)
diff --git a/relint.el b/relint.el
index 8759d05..1c32d87 100644
--- a/relint.el
+++ b/relint.el
@@ -204,7 +204,7 @@ and PATH (reversed list of list indices to follow to
target)."
(relint--check-string re #'xr-lint name file pos path))
(defvar relint--variables nil
- "Alist of variable definitions seen so far.
+ "Alist of global variable definitions seen so far.
The variable names map to unevaluated forms.")
@@ -229,6 +229,11 @@ and PATH (reversed list of list indices to follow to
target)."
;; Alist of alias definitions in the current file.
(defvar relint--alias-defs)
+;; Alist of local variables. Each element is either (NAME VALUE),
+;; where VALUE is the (evaluated) value, or just (NAME) if the binding
+;; exists but the value is unknown.
+(defvar relint--locals)
+
(defconst relint--safe-functions
'(cons list append
concat
@@ -323,8 +328,18 @@ alternatives. They may still require wrapping their
function arguments.")
(cdr args))))
(condition-case err
(apply #'rx-to-string safe-args)
- (error (signal 'relint--eval-error
- (format "rx error: %s" (cadr err)))))))
+ (error
+ ;; "Unknown rx form" errors are probably just the result of our
+ ;; evaluation not applying extensions to `rx-constituents' etc;
+ ;; treat them as failed evaluation, not as errors to be signalled.
+ ;; FIXME: Is there any other kind? If not, we can do away with
+ ;; the entire `relint--eval-error' business.
+ (if (and (eq (car err) 'error)
+ (stringp (cadr err))
+ (string-prefix-p "Unknown rx form" (cadr err)))
+ (throw 'relint-eval 'no-value)
+ (signal 'relint--eval-error
+ (format "rx error: %s" (cadr err))))))))
(defun relint--apply (formals actuals expr)
"Bind FORMALS to ACTUALS and evaluate EXPR."
@@ -332,17 +347,17 @@ alternatives. They may still require wrapping their
function arguments.")
(while formals
(cond
((eq (car formals) '&rest)
- (push (cons (cadr formals) (list 'quote actuals)) bindings)
+ (push (cons (cadr formals) (list actuals)) bindings)
(setq formals nil))
((eq (car formals) '&optional)
(setq formals (cdr formals)))
(t
- (push (cons (car formals) (list 'quote (car actuals))) bindings)
+ (push (cons (car formals) (list (car actuals))) bindings)
(setq formals (cdr formals))
(setq actuals (cdr actuals)))))
;; This results in dynamic binding, but that doesn't matter for our
;; purposes.
- (let ((relint--variables (append bindings relint--variables)))
+ (let ((relint--locals (append bindings relint--locals)))
(relint--eval expr))))
(defun relint--no-value (&rest _)
@@ -389,6 +404,12 @@ into something that can be called safely."
(plist-put ret :key (relint--wrap-function key)))
ret))
+(defun relint--eval-to-binding (form)
+ "Evaluate a form, returning (VALUE) on success or nil on failure."
+ (let ((val (catch 'relint-eval
+ (list (relint--eval form)))))
+ (if (eq val 'no-value) nil val)))
+
(defun relint--eval (form)
"Evaluate a form. Throw 'relint-eval 'no-value if something could
not be evaluated safely."
@@ -396,11 +417,15 @@ not be evaluated safely."
(cond
((booleanp form) form)
((symbolp form)
- (and form
- (let ((binding (assq form relint--variables)))
- (if binding
- (relint--eval (cdr binding))
- (throw 'relint-eval 'no-value)))))
+ (let ((local (assq form relint--locals)))
+ (if local
+ (if (cdr local)
+ (cadr local)
+ (throw 'relint-eval 'no-value))
+ (let ((binding (assq form relint--variables)))
+ (if binding
+ (relint--eval (cdr binding))
+ (throw 'relint-eval 'no-value))))))
(t form))
(let ((head (car form))
(body (cdr form)))
@@ -593,10 +618,10 @@ not be evaluated safely."
(mapcar (lambda (binding)
(if (consp binding)
(cons (car binding)
- (list 'quote (relint--eval (cadr binding))))
- (cons binding nil)))
+ (relint--eval-to-binding (cadr binding)))
+ (cons binding (list nil))))
(car body))))
- (let ((relint--variables (append bindings relint--variables)))
+ (let ((relint--locals (append bindings relint--locals)))
(relint--eval (car (last body))))))
((eq head 'let*)
@@ -605,13 +630,13 @@ not be evaluated safely."
(let ((bindings (car body)))
(if bindings
(let* ((binding (car bindings))
- (relint--variables
+ (relint--locals
(cons
(if (consp binding)
(cons (car binding)
- (list 'quote (relint--eval (cadr binding))))
- (cons binding nil))
- relint--variables)))
+ (relint--eval-to-binding (cadr binding)))
+ (cons binding (list nil)))
+ relint--locals)))
(relint--eval `(let* ,(cdr bindings) ,@(cdr body))))
(relint--eval (car (last body))))))
@@ -686,8 +711,11 @@ evaluated are nil."
(cond
((symbolp form)
(and form
- (let ((val (cdr (assq form relint--variables))))
- (and val (relint--eval-list val)))))
+ (let ((local (assq form relint--locals)))
+ (if local
+ (and (cdr local) (cadr local))
+ (let ((val (cdr (assq form relint--variables))))
+ (and val (relint--eval-list val)))))))
((atom form)
form)
((eq (car form) 'eval-when-compile)
@@ -989,162 +1017,237 @@ character alternative: `[' followed by a
regexp-generating expression."
(and (consp type)
(eq (car type) 'regexp))))
+(defun relint--check-and-eval-let-binding (binding file pos path)
+ "Check the let-binding BINDING, which is probably (NAME EXPR) or NAME,
+and evaluate EXPR. On success return (NAME VALUE); if evaluation failed,
+return (NAME); on syntax error, return nil."
+ (cond ((symbolp binding)
+ (cons binding (list nil)))
+ ((and (consp binding)
+ (symbolp (car binding))
+ (consp (cdr binding)))
+ (relint--check-form-recursively-2
+ (cadr binding) file pos (cons 1 path))
+ (let ((val (catch 'relint-eval
+ (list (relint--eval (cadr binding))))))
+ (cons (car binding)
+ (if (eq val 'no-value)
+ nil
+ val))))))
+
+(defun relint--check-let* (bindings body file pos path index)
+ "Check the BINDINGS and BODY of a `let*' form."
+ (if bindings
+ (let ((b (relint--check-and-eval-let-binding
+ (car bindings) file pos (cons index (cons 1 path)))))
+ (if b
+ (let ((relint--locals (cons b relint--locals)))
+ (relint--check-let* (cdr bindings) body file pos path (1+
index)))
+ (relint--check-let* (cdr bindings) body file pos path (1+ index))))
+ (let ((index 2))
+ (while (consp body)
+ (when (consp (car body))
+ (relint--check-form-recursively-2
+ (car body) file pos (cons index path)))
+ (setq body (cdr body))
+ (setq index (1+ index))))))
+
(defun relint--check-form-recursively-2 (form file pos path)
(pcase form
- (`(,(or 'looking-at 're-search-forward 're-search-backward
- 'string-match 'string-match-p 'looking-back 'looking-at-p
- 'replace-regexp-in-string 'replace-regexp
- 'query-replace-regexp
- 'posix-looking-at 'posix-search-backward 'posix-search-forward
- 'posix-string-match
- 'load-history-filename-element
- 'kill-matching-buffers
- 'keep-lines 'flush-lines 'how-many)
- ,re-arg . ,_)
- (unless (and (symbolp re-arg)
- (memq re-arg relint--checked-variables))
- (relint--check-re re-arg (format "call to %s" (car form))
- file pos (cons 1 path))))
- (`(,(or 'split-string 'split-string-and-unquote
- 'string-trim-left 'string-trim-right 'string-trim
- 'directory-files-recursively)
- ,_ ,re-arg . ,rest)
- (unless (and (symbolp re-arg)
- (memq re-arg relint--checked-variables))
- (relint--check-re re-arg (format "call to %s" (car form))
- file pos (cons 2 path)))
- ;; string-trim has another regexp argument (trim-right, arg 3)
- (when (and (eq (car form) 'string-trim)
- (car rest))
- (let ((right (car rest)))
- (unless (and (symbolp right)
- (memq right relint--checked-variables))
- (relint--check-re right (format "call to %s" (car form))
- file pos (cons 3 path)))))
- ;; split-string has another regexp argument (trim, arg 4)
- (when (and (eq (car form) 'split-string)
- (cadr rest))
- (let ((trim (cadr rest)))
- (unless (and (symbolp trim)
- (memq trim relint--checked-variables))
- (relint--check-re trim (format "call to %s" (car form))
- file pos (cons 4 path))))))
- (`(,(or 'skip-chars-forward 'skip-chars-backward)
- ,skip-arg . ,_)
- (let ((str (relint--get-string skip-arg file pos path)))
- (when str
- (relint--check-skip-set str (format "call to %s" (car form))
- file pos (cons 1 path))))
- (relint--check-skip-set-provenance
- (car form) skip-arg file pos (cons 1 path))
- )
- (`(concat . ,args)
- (relint--check-concat-mixup args file pos path))
- (`(format ,template-arg . ,args)
- (let ((template (relint--get-string template-arg file pos path)))
- (when template
- (relint--check-format-mixup template args file pos path))))
- (`(,(or 'defvar 'defconst 'defcustom)
- ,name ,re-arg . ,rest)
- (let ((type (and (eq (car form) 'defcustom)
- (relint--eval-or-nil (plist-get (cdr rest) :type)))))
- (when (symbolp name)
- (cond
- ((or (relint--defcustom-type-regexp-p type)
- (string-match-p (rx (or "-regexp" "-re" "-regex" "-pattern")
eos)
- (symbol-name name)))
- (relint--check-re re-arg name file pos (cons 2 path))
- (when (eq (car form) 'defcustom)
- (relint--check-defcustom-re form name file pos path))
- (push name relint--checked-variables))
- ((and (consp type)
- (eq (car type) 'alist)
- (relint--defcustom-type-regexp-p
- (plist-get (cdr type) :key-type)))
- (relint--check-list-any re-arg name file pos (cons 2 path))
- (push name relint--checked-variables))
- ((and (consp type)
- (eq (car type) 'alist)
- (relint--defcustom-type-regexp-p
- (plist-get (cdr type) :value-type)))
- (relint--check-alist-cdr re-arg name file pos (cons 2 path))
- (push name relint--checked-variables))
- ((or (and (consp type)
- (eq (car type) 'repeat)
- (relint--defcustom-type-regexp-p (cadr type)))
- (string-match-p (rx (or (or "-regexps" "-regexes")
- (seq (or "-regexp" "-re" "-regex")
- "-list"))
- eos)
- (symbol-name name)))
- (relint--check-list re-arg name file pos (cons 2 path))
- (push name relint--checked-variables))
- ((string-match-p (rx "-font-lock-keywords" eos)
- (symbol-name name))
- (relint--check-font-lock-keywords re-arg name file pos (cons 2
path))
- (push name relint--checked-variables))
- ((eq name 'compilation-error-regexp-alist-alist)
- (relint--check-compilation-error-regexp-alist-alist
- re-arg name file pos (cons 2 path))
- (push name relint--checked-variables))
- ((string-match-p (rx (or "-regexp" "-re" "-regex" "-pattern")
- "-alist" eos)
- (symbol-name name))
- (relint--check-list-any re-arg name file pos (cons 2 path))
- (push name relint--checked-variables))
- ((string-match-p (rx "-mode-alist" eos)
- (symbol-name name))
- (relint--check-list-any re-arg name file pos (cons 2 path))
- (push name relint--checked-variables))
- ((string-match-p (rx "-rules-list" eos)
- (symbol-name name))
- (relint--check-rules-list re-arg name file pos (cons 2 path))
- (push name relint--checked-variables))
- ;; Doc string starting with "regexp"?
- ((and (stringp (car rest))
- (let ((case-fold-search t))
- (string-match-p (rx bos "regexp") (car rest))))
- (relint--check-re re-arg name file pos (cons 2 path))
- (when (eq (car form) 'defcustom)
- (relint--check-defcustom-re form name file pos path))
- (push name relint--checked-variables))
- )
- (push (cons name re-arg) relint--variables))))
- (`(define-generic-mode ,name ,_ ,_ ,font-lock-list ,auto-mode-list . ,_)
- (let ((origin (format "define-generic-mode %s" name)))
- (relint--check-font-lock-keywords font-lock-list origin
- file pos (cons 4 path))
- (relint--check-list auto-mode-list origin file pos (cons 5 path))))
- (`(,name . ,args)
- (let ((alias (assq name relint--alias-defs)))
- (when alias
+ (`(let ,(and (pred listp) bindings) . ,body)
+ (let* ((i 0)
+ (new-bindings
+ (mapcan (lambda (binding)
+ (let ((b (relint--check-and-eval-let-binding
+ binding file pos
+ (cons i (cons 1 path)))))
+ (setq i (1+ i))
+ (and b (list b))))
+ bindings)))
+ (let ((relint--locals
+ (append (nreverse new-bindings) relint--locals))
+ (index 2))
+ (while (consp body)
+ (when (consp (car body))
+ (relint--check-form-recursively-2
+ (car body) file pos (cons index path)))
+ (setq body (cdr body))
+ (setq index (1+ index))))))
+ (`(let* ,(and (pred listp) bindings) . ,body)
+ (relint--check-let* bindings body file pos path 0))
+ (`(setq . ,args)
+ ;; Since we don't keep track on program flow (loops, conditions etc),
+ ;; we cannot reassign variables properly. Do the next best: treat every
+ ;; `setq' as an invalidation of the variable value.
+ (let ((i 2))
+ (while (and (consp args) (consp (cdr args)) (symbolp (car args)))
(relint--check-form-recursively-2
- (cons (cdr alias) args) file pos path))))
- )
-
- ;; Check calls to remembered functions with regexp arguments.
- (when (consp form)
- (let ((indices (cdr (assq (car form) relint--regexp-functions))))
- (when indices
- (let ((index 0)
- (args (cdr form)))
- (while (and indices (consp args))
- (when (= index (car indices))
- (unless (and (symbolp (car args))
- (memq (car args) relint--checked-variables))
- (relint--check-re (car args) (format "call to %s" (car form))
- file pos (cons (1+ index) path)))
- (setq indices (cdr indices)))
- (setq args (cdr args))
- (setq index (1+ index)))))))
-
- (let ((index 0))
- (while (consp form)
- (when (consp (car form))
- (relint--check-form-recursively-2
- (car form) file pos (cons index path)))
- (setq form (cdr form))
- (setq index (1+ index)))))
+ (cadr args) file pos (cons i path))
+ ;; Invalidate the variable if it was local; otherwise, ignore.
+ (let ((local (assq (car args) relint--locals)))
+ (when local
+ (setcdr local nil)))
+ (setq args (cddr args))
+ (setq i (+ i 2)))))
+ (_
+ (pcase form
+ (`(,(or 'looking-at 're-search-forward 're-search-backward
+ 'string-match 'string-match-p 'looking-back 'looking-at-p
+ 'replace-regexp-in-string 'replace-regexp
+ 'query-replace-regexp
+ 'posix-looking-at 'posix-search-backward 'posix-search-forward
+ 'posix-string-match
+ 'load-history-filename-element
+ 'kill-matching-buffers
+ 'keep-lines 'flush-lines 'how-many)
+ ,re-arg . ,_)
+ (unless (and (symbolp re-arg)
+ (memq re-arg relint--checked-variables))
+ (relint--check-re re-arg (format "call to %s" (car form))
+ file pos (cons 1 path))))
+ (`(,(or 'split-string 'split-string-and-unquote
+ 'string-trim-left 'string-trim-right 'string-trim
+ 'directory-files-recursively)
+ ,_ ,re-arg . ,rest)
+ (unless (and (symbolp re-arg)
+ (memq re-arg relint--checked-variables))
+ (relint--check-re re-arg (format "call to %s" (car form))
+ file pos (cons 2 path)))
+ ;; string-trim has another regexp argument (trim-right, arg 3)
+ (when (and (eq (car form) 'string-trim)
+ (car rest))
+ (let ((right (car rest)))
+ (unless (and (symbolp right)
+ (memq right relint--checked-variables))
+ (relint--check-re right (format "call to %s" (car form))
+ file pos (cons 3 path)))))
+ ;; split-string has another regexp argument (trim, arg 4)
+ (when (and (eq (car form) 'split-string)
+ (cadr rest))
+ (let ((trim (cadr rest)))
+ (unless (and (symbolp trim)
+ (memq trim relint--checked-variables))
+ (relint--check-re trim (format "call to %s" (car form))
+ file pos (cons 4 path))))))
+ (`(,(or 'skip-chars-forward 'skip-chars-backward)
+ ,skip-arg . ,_)
+ (let ((str (relint--get-string skip-arg file pos path)))
+ (when str
+ (relint--check-skip-set str (format "call to %s" (car form))
+ file pos (cons 1 path))))
+ (relint--check-skip-set-provenance
+ (car form) skip-arg file pos (cons 1 path))
+ )
+ (`(concat . ,args)
+ (relint--check-concat-mixup args file pos path))
+ (`(format ,template-arg . ,args)
+ (let ((template (relint--get-string template-arg file pos path)))
+ (when template
+ (relint--check-format-mixup template args file pos path))))
+ (`(,(or 'defvar 'defconst 'defcustom)
+ ,name ,re-arg . ,rest)
+ (let ((type (and (eq (car form) 'defcustom)
+ (relint--eval-or-nil (plist-get (cdr rest) :type)))))
+ (when (symbolp name)
+ (cond
+ ((or (relint--defcustom-type-regexp-p type)
+ (string-match-p (rx (or "-regexp" "-re" "-regex" "-pattern")
+ eos)
+ (symbol-name name)))
+ (relint--check-re re-arg name file pos (cons 2 path))
+ (when (eq (car form) 'defcustom)
+ (relint--check-defcustom-re form name file pos path))
+ (push name relint--checked-variables))
+ ((and (consp type)
+ (eq (car type) 'alist)
+ (relint--defcustom-type-regexp-p
+ (plist-get (cdr type) :key-type)))
+ (relint--check-list-any re-arg name file pos (cons 2 path))
+ (push name relint--checked-variables))
+ ((and (consp type)
+ (eq (car type) 'alist)
+ (relint--defcustom-type-regexp-p
+ (plist-get (cdr type) :value-type)))
+ (relint--check-alist-cdr re-arg name file pos (cons 2 path))
+ (push name relint--checked-variables))
+ ((or (and (consp type)
+ (eq (car type) 'repeat)
+ (relint--defcustom-type-regexp-p (cadr type)))
+ (string-match-p (rx (or (or "-regexps" "-regexes")
+ (seq (or "-regexp" "-re" "-regex")
+ "-list"))
+ eos)
+ (symbol-name name)))
+ (relint--check-list re-arg name file pos (cons 2 path))
+ (push name relint--checked-variables))
+ ((string-match-p (rx "-font-lock-keywords" eos)
+ (symbol-name name))
+ (relint--check-font-lock-keywords re-arg name file pos
+ (cons 2 path))
+ (push name relint--checked-variables))
+ ((eq name 'compilation-error-regexp-alist-alist)
+ (relint--check-compilation-error-regexp-alist-alist
+ re-arg name file pos (cons 2 path))
+ (push name relint--checked-variables))
+ ((string-match-p (rx (or "-regexp" "-re" "-regex" "-pattern")
+ "-alist" eos)
+ (symbol-name name))
+ (relint--check-list-any re-arg name file pos (cons 2 path))
+ (push name relint--checked-variables))
+ ((string-match-p (rx "-mode-alist" eos)
+ (symbol-name name))
+ (relint--check-list-any re-arg name file pos (cons 2 path))
+ (push name relint--checked-variables))
+ ((string-match-p (rx "-rules-list" eos)
+ (symbol-name name))
+ (relint--check-rules-list re-arg name file pos (cons 2 path))
+ (push name relint--checked-variables))
+ ;; Doc string starting with "regexp"?
+ ((and (stringp (car rest))
+ (let ((case-fold-search t))
+ (string-match-p (rx bos "regexp") (car rest))))
+ (relint--check-re re-arg name file pos (cons 2 path))
+ (when (eq (car form) 'defcustom)
+ (relint--check-defcustom-re form name file pos path))
+ (push name relint--checked-variables))
+ )
+ (push (cons name re-arg) relint--variables))))
+ (`(define-generic-mode ,name ,_ ,_ ,font-lock-list ,auto-mode-list . ,_)
+ (let ((origin (format "define-generic-mode %s" name)))
+ (relint--check-font-lock-keywords font-lock-list origin
+ file pos (cons 4 path))
+ (relint--check-list auto-mode-list origin file pos (cons 5 path))))
+ (`(,name . ,args)
+ (let ((alias (assq name relint--alias-defs)))
+ (when alias
+ (relint--check-form-recursively-2
+ (cons (cdr alias) args) file pos path))))
+ )
+
+ ;; Check calls to remembered functions with regexp arguments.
+ (when (consp form)
+ (let ((indices (cdr (assq (car form) relint--regexp-functions))))
+ (when indices
+ (let ((index 0)
+ (args (cdr form)))
+ (while (and indices (consp args))
+ (when (= index (car indices))
+ (unless (and (symbolp (car args))
+ (memq (car args) relint--checked-variables))
+ (relint--check-re (car args)
+ (format "call to %s" (car form))
+ file pos (cons (1+ index) path)))
+ (setq indices (cdr indices)))
+ (setq args (cdr args))
+ (setq index (1+ index)))))))
+
+ (let ((index 0))
+ (while (consp form)
+ (when (consp (car form))
+ (relint--check-form-recursively-2
+ (car form) file pos (cons index path)))
+ (setq form (cdr form))
+ (setq index (1+ index)))))))
(defun relint--show-errors ()
(unless noninteractive
@@ -1191,6 +1294,7 @@ Return a list of (FORM . STARTING-POSITION)."
(relint--function-defs nil)
(relint--macro-defs nil)
(relint--alias-defs nil)
+ (relint--locals nil)
(case-fold-search nil))
(dolist (form forms)
(relint--check-form-recursively-1 (car form) file (cdr form) nil))
- [elpa] externals/relint updated (e049e93 -> bc001eb), Mattias Engdegård, 2019/08/04
- [elpa] externals/relint c5ac726 03/10: Handle rx `literal' and `regexp' forms correctly, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint 77c3b87 04/10: Improved source traversal towards location, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint 1dc96b5 05/10: Make font-lock-keywords errors easier to find, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint bc001eb 10/10: Increment version to 1.9, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint 963e232 02/10: Check more defcustom strings, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint c08baf2 06/10: Quote symbols with ' instead of ` in pcase, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint 9238375 07/10: Detect more regexps in defcustom alists, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint 943c728 08/10: Indentation fixes, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint 298d407 01/10: Restructure `cond' expressions for better compilation, Mattias Engdegård, 2019/08/04
- [elpa] externals/relint 2e0bb22 09/10: Keep track of local variable bindings,
Mattias Engdegård <=