[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/elpa ef6c483 13/71: Fix #452: also check types when des
From: |
João Távora |
Subject: |
[elpa] externals/elpa ef6c483 13/71: Fix #452: also check types when destructuring LSP objects |
Date: |
Wed, 16 Dec 2020 11:42:15 -0500 (EST) |
branch: externals/elpa
commit ef6c48328836434fa4dca68450c72aba2601a357
Author: João Távora <joaotavora@gmail.com>
Commit: João Távora <joaotavora@gmail.com>
Fix #452: also check types when destructuring LSP objects
The problem in this issue is that the disambiguation between Command
and CodeAction objects can only be performed by checking the types of
the keys involved. So we added that to the spec and check it at
runtime.
* eglot.el (eglot--lsp-interface-alist): Add types to
Command. Tweak docstring.
(eglot--check-object): Renamed from eglot--call-with-interface.
(eglot--ensure-type): New helper.
(eglot--interface): New helper.
(eglot--check-dspec): Renamed from eglot--check-interface.
(eglot--dbind): Simplify.
(eglot-code-actions): Adjust indentation.
* eglot-tests.el (eglot-dcase-issue-452): New test.
---
eglot-tests.el | 14 +++++++
eglot.el | 118 +++++++++++++++++++++++++++++++++------------------------
2 files changed, 82 insertions(+), 50 deletions(-)
diff --git a/eglot-tests.el b/eglot-tests.el
index ea5a9cd..f2a9b7f 100644
--- a/eglot-tests.el
+++ b/eglot-tests.el
@@ -866,6 +866,20 @@ pyls prefers autopep over yafp, despite its README stating
the contrary."
(((CodeAction) _title _edit _command)
(ert-fail "Shouldn't have destructured this object as a
CodeAction")))))))
+(ert-deftest eglot-dcase-issue-452 ()
+ (let ((eglot--lsp-interface-alist
+ `((FooObject . ((:foo :bar) (:baz)))
+ (CodeAction (:title) (:kind :diagnostics :edit :command))
+ (Command ((string . :title) (:command . string)) (:arguments)))))
+ (should
+ (equal
+ (list "foo" '(:command "cmd" :title "alsofoo"))
+ (eglot--dcase '(:title "foo" :command (:command "cmd" :title "alsofoo"))
+ (((Command) _title _command _arguments)
+ (ert-fail "Shouldn't have destructured this object as a Command"))
+ (((CodeAction) title command)
+ (list title command)))))))
+
(provide 'eglot-tests)
;;; eglot-tests.el ends here
diff --git a/eglot.el b/eglot.el
index c485b4e..42fca9b 100644
--- a/eglot.el
+++ b/eglot.el
@@ -231,7 +231,7 @@ let the buffer grow forever."
`(
(CodeAction (:title) (:kind :diagnostics :edit :command))
(ConfigurationItem () (:scopeUri :section))
- (Command (:title :command) (:arguments))
+ (Command ((:title . string) (:command . string)) (:arguments))
(CompletionItem (:label)
(:kind :detail :documentation :deprecated :preselect
:sortText :filterText :insertText
:insertTextFormat
@@ -265,13 +265,15 @@ let the buffer grow forever."
INTERFACE-NAME is a symbol designated by the spec as
\"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where
-REQUIRED and OPTIONAL are lists of keyword symbols designating
-field names that must be, or may be, respectively, present in a
-message adhering to that interface.
+REQUIRED and OPTIONAL are lists of KEYWORD designating field
+names that must be, or may be, respectively, present in a message
+adhering to that interface. KEY can be a keyword or a cons (SYM
+TYPE), where type is used by `cl-typep' to check types at
+runtime.
Here's what an element of this alist might look like:
- (CreateFile . ((:kind :uri) (:options)))"))
+ (Command ((:title . string) (:command . string)) (:arguments))"))
(eval-and-compile
(defvar eglot-strict-mode (if load-file-name '()
@@ -308,46 +310,69 @@ on unknown notifications and errors on unknown requests.
(defun eglot--plist-keys (plist)
(cl-loop for (k _v) on plist by #'cddr collect k))
-(defun eglot--call-with-interface (interface object fn)
- "Call FN, checking that OBJECT conforms to INTERFACE."
- (when-let ((missing (and (memq 'enforce-required-keys eglot-strict-mode)
- (cl-set-difference (car (cdr interface))
- (eglot--plist-keys object)))))
- (eglot--error "A `%s' must have %s" (car interface) missing))
- (when-let ((excess (and (memq 'disallow-non-standard-keys eglot-strict-mode)
- (cl-set-difference
- (eglot--plist-keys object)
- (append (car (cdr interface)) (cadr (cdr
interface)))))))
- (eglot--error "A `%s' mustn't have %s" (car interface) excess))
- (funcall fn))
+(cl-defun eglot--check-object (interface-name
+ object
+ &optional
+ (enforce-required t)
+ (disallow-non-standard t)
+ (check-types t))
+ "Check that OBJECT conforms to INTERFACE. Error otherwise."
+ (cl-destructuring-bind
+ (&key types required-keys optional-keys &allow-other-keys)
+ (eglot--interface interface-name)
+ (when-let ((missing (and enforce-required
+ (cl-set-difference required-keys
+ (eglot--plist-keys object)))))
+ (eglot--error "A `%s' must have %s" interface-name missing))
+ (when-let ((excess (and disallow-non-standard
+ (cl-set-difference
+ (eglot--plist-keys object)
+ (append required-keys optional-keys)))))
+ (eglot--error "A `%s' mustn't have %s" interface-name excess))
+ (when check-types
+ (cl-loop
+ for (k v) on object by #'cddr
+ for type = (or (cdr (assoc k types)) t) ;; FIXME: enforce nil type?
+ unless (cl-typep v type)
+ do (eglot--error "A `%s' must have a %s as %s, but has %s"
+ interface-name )))
+ t))
(eval-and-compile
(defun eglot--keywordize-vars (vars)
(mapcar (lambda (var) (intern (format ":%s" var))) vars))
- (defun eglot--check-interface (interface-name vars)
- (let ((interface
- (assoc interface-name eglot--lsp-interface-alist)))
- (cond (interface
+ (defun eglot--ensure-type (k) (if (consp k) k (cons k t)))
+
+ (defun eglot--interface (interface-name)
+ (let* ((interface (assoc interface-name eglot--lsp-interface-alist))
+ (required (mapcar #'eglot--ensure-type (car (cdr interface))))
+ (optional (mapcar #'eglot--ensure-type (cadr (cdr interface)))))
+ (list :types (append required optional)
+ :required-keys (mapcar #'car required)
+ :optional-keys (mapcar #'car optional))))
+
+ (defun eglot--check-dspec (interface-name dspec)
+ "Check if variables in DSPEC "
+ (cl-destructuring-bind (&key required-keys optional-keys &allow-other-keys)
+ (eglot--interface interface-name)
+ (cond ((or required-keys optional-keys)
(let ((too-many
(and
(memq 'disallow-non-standard-keys eglot-strict-mode)
(cl-set-difference
- (eglot--keywordize-vars vars)
- (append (car (cdr interface))
- (cadr (cdr interface))))))
+ (eglot--keywordize-vars dspec)
+ (append required-keys optional-keys))))
(ignored-required
(and
(memq 'enforce-required-keys eglot-strict-mode)
(cl-set-difference
- (car (cdr interface))
- (eglot--keywordize-vars vars))))
+ required-keys (eglot--keywordize-vars dspec))))
(missing-out
(and
(memq 'enforce-optional-keys eglot-strict-mode)
(cl-set-difference
- (cadr (cdr interface))
- (eglot--keywordize-vars vars)))))
+ optional-keys (eglot--keywordize-vars dspec)))))
(when too-many (byte-compile-warn
"Destructuring for %s has extraneous %s"
interface-name too-many))
@@ -361,7 +386,7 @@ on unknown notifications and errors on unknown requests.
(byte-compile-warn "Unknown LSP interface %s" interface-name))))))
(cl-defmacro eglot--dbind (vars object &body body)
- "Destructure OBJECT of binding VARS in BODY.
+ "Destructure OBJECT, binding VARS in BODY.
VARS is ([(INTERFACE)] SYMS...)
Honour `eglot-strict-mode'."
(declare (indent 2) (debug (sexp sexp &rest form)))
@@ -370,13 +395,14 @@ Honour `eglot-strict-mode'."
(object-once (make-symbol "object-once"))
(fn-once (make-symbol "fn-once")))
(cond (interface-name
- (eglot--check-interface interface-name vars)
+ (eglot--check-dspec interface-name vars)
`(let ((,object-once ,object))
(cl-destructuring-bind (&key ,@vars &allow-other-keys)
,object-once
- (eglot--call-with-interface (assoc ',interface-name
- eglot--lsp-interface-alist)
- ,object-once (lambda ()
- ,@body)))))
+ (eglot--check-object ',interface-name ,object-once
+ (memq 'enforce-required-keys
eglot-strict-mode)
+ (memq 'disallow-non-standard-keys
eglot-strict-mode)
+ (memq 'check-types eglot-strict-mode))
+ ,@body)))
(t
`(let ((,object-once ,object)
(,fn-once (lambda (,@vars) ,@body)))
@@ -409,20 +435,12 @@ treated as in `eglot-dbind'."
(car (pop vars)))
for condition =
(cond (interface-name
- (eglot--check-interface interface-name vars)
+ (eglot--check-dspec interface-name vars)
;; In this mode, in runtime, we assume
;; `eglot-strict-mode' is fully on, otherwise we
;; can't disambiguate between certain types.
- `(let* ((interface
- (or (assoc ',interface-name
eglot--lsp-interface-alist)
- (eglot--error "Unknown LSP interface %s"
- ',interface-name)))
- (object-keys (eglot--plist-keys ,obj-once))
- (required-keys (car (cdr interface))))
- (and (null (cl-set-difference required-keys object-keys))
- (null (cl-set-difference
- (cl-set-difference object-keys required-keys)
- (cadr (cdr interface)))))))
+ `(ignore-errors
+ (eglot--check-object ',interface-name ,obj-once)))
(t
;; In this interface-less mode we don't check
;; `eglot-strict-mode' at all: just check that the object
@@ -435,7 +453,7 @@ treated as in `eglot-dbind'."
,obj-once
,@body)))
(t
- (eglot--error "%s didn't match any of %s"
+ (eglot--error "%S didn't match any of %S"
,obj-once
',(mapcar #'car clauses)))))))
@@ -2499,12 +2517,12 @@ echo area cleared of any previous documentation."
(action (if (listp last-nonmenu-event)
(x-popup-menu last-nonmenu-event menu)
(cdr (assoc (completing-read "[eglot] Pick an action: "
- menu-items nil t
- nil nil (car menu-items))
+ menu-items nil t
+ nil nil (car menu-items))
menu-items)))))
(eglot--dcase action
- (((Command) command arguments)
- (eglot-execute-command server (intern command) arguments))
+ (((Command) command arguments)
+ (eglot-execute-command server (intern command) arguments))
(((CodeAction) edit command)
(when edit (eglot--apply-workspace-edit edit))
(when command
- [elpa] branch externals/elpa created (now 4edd478), João Távora, 2020/12/16
- [elpa] externals/elpa 8b94cf4 02/71: Per #397: Add new eglot-lsp-abiding-column test, João Távora, 2020/12/16
- [elpa] externals/elpa 1056ef5 03/71: Close #413: update dependencies and copyright years, João Távora, 2020/12/16
- [elpa] externals/elpa d99a447 12/71: Close #439: Hide eldoc-message on empty hover info, João Távora, 2020/12/16
- [elpa] externals/elpa af32ebf 06/71: Close #444: use text-mode for plaintext markup, João Távora, 2020/12/16
- [elpa] externals/elpa ef6c483 13/71: Fix #452: also check types when destructuring LSP objects,
João Távora <=
- [elpa] externals/elpa 487cde5 18/71: Close #443: kind of honour eldoc-echo-area-use-multiline-p, João Távora, 2020/12/16
- [elpa] externals/elpa 22aa27c 01/71: Close #397: Simplify a bit of code, João Távora, 2020/12/16
- [elpa] externals/elpa 8900a35 04/71: Prevent "Cant guess python-indent-offset..." messages in tests, João Távora, 2020/12/16
- [elpa] externals/elpa dd48f4a 08/71: * eglot.el (eglot-put-doc-in-help-buffer): Tiny docstring fix., João Távora, 2020/12/16
- [elpa] externals/elpa e5cf30e 09/71: Close #435: create match xrefs when possible, João Távora, 2020/12/16
- [elpa] externals/elpa ad3f049 24/71: Close #417: minimally document relation with project.el, João Távora, 2020/12/16
- [elpa] externals/elpa 2f75da2 28/71: Fix #460: fix "free variable" warning, João Távora, 2020/12/16
- [elpa] externals/elpa 0f57efb 26/71: Simplify bug-reporting instructions, João Távora, 2020/12/16
- [elpa] externals/elpa 91a7cba 33/71: Fix #474, #478: prompt for executable if supplied name does not exist, João Távora, 2020/12/16
- [elpa] externals/elpa 9efe207 05/71: Make curl invocation fail more explicitly in .travis.yml, João Távora, 2020/12/16