magit / transient

Transient commands

Home Page:https://magit.vc/manual/transient

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

"Invalid slot name" signal when using :incompatible with options with suffix but no args

stevemolitor opened this issue · comments

Description

When using :incompatible with another option that has defined suffix no argument, if that option comes before the incompatible options, I get an (invalid-slot-name "#<transient-suffix transient-suffix-53012386>" argument) error.

Here is the transient definition:

(require 'transient)

(transient-define-suffix simple-suffix ()
  (interactive)
  (message "args %S" (transient-args transient-current-command)))

(transient-define-prefix incompatible-bug ()
  :incompatible '(("a=" "b="))
  [""
   ("x" "exit" simple-suffix)
   ("a" "a" "a=")
   ("b" "b" "b=")])

The error happens in transient-infix-set here:

(and (slot-boundp obj 'argument)

slot-boundp calls eieio-oref here:

(eieio-oref object slot)

If the slot is missing, eieio-oref calls slot-missing here

	    ;; The slot-missing method is a cool way of allowing an object author
	    ;; to intercept missing slot definitions.  Since it is also the LAST
	    ;; thing called in this fn, its return value would be retrieved.
	    (slot-missing obj slot 'oref))

slot-missing in turn signals invalid-slot-name here:

(signal 'invalid-slot-name

One option would be to replace slot-boundp with slot-exists-p in transient. Or intercept the missing slot definition somehow as described in the slot-missing comment in eieo above.

The eieio behavior is odd to me - a bound predicate shouldn't signal an error IMHO. 🤷‍♂️

Versions

emacs version: 29.1 pretest 2, https://github.com/emacs-mirror/emacs/blob/3aef46e466fd8e9032fe6565ea030653b1ec0cff
magit-version: Magit v3.3.0-586-ga760dd10, Git 2.38.1, Emacs 29.0.91, darwin - Emacs 29.1 pretest 2
transient version: 0.4.0
eieio-version: 1.4 from Emacs 29.1 pretest 2

Trace

Debugger entered--Lisp error: (invalid-slot-name "#<transient-suffix transient-suffix-45c3bac0>" argument)
  signal(invalid-slot-name ("#<transient-suffix transient-suffix-45c3bac0>" argument))
  slot-missing(#<transient-suffix transient-suffix-45c3bac0> argument oref)
  eieio-oref(#<transient-suffix transient-suffix-45c3bac0> argument)
  slot-boundp(#<transient-suffix transient-suffix-45c3bac0> argument)
  #f(compiled-function (obj) #<bytecode -0x1adb69aa9167e36>)(#<transient-suffix transient-suffix-45c3bac0>)
  cl--position(nil (#<transient-suffix transient-suffix-45c3bac0> #<transient-option transient-option-45c3bafc> #<transient-option transient-option-49c84a12> #<transient-suffix transient-suffix-49c84b12> #<transient-suffix transient-suffix-49c84b4e> #<transient-suffix transient-suffix-49c84b7a> #<transient-suffix transient-suffix-49c84bb6> #<transient-suffix transient-suffix-49c84be2> #<transient-suffix transient-suffix-49c84c48> #<transient-suffix transient-suffix-49c84c84> #<transient-suffix transient-suffix-49c84cb0> #<transient-suffix transient-suffix-49c84d1e> #<transient-suffix transient-suffix-49c84d4a>) 0 nil nil)
  cl-position(nil (#<transient-suffix transient-suffix-45c3bac0> #<transient-option transient-option-45c3bafc> #<transient-option transient-option-49c84a12> #<transient-suffix transient-suffix-49c84b12> #<transient-suffix transient-suffix-49c84b4e> #<transient-suffix transient-suffix-49c84b7a> #<transient-suffix transient-suffix-49c84bb6> #<transient-suffix transient-suffix-49c84be2> #<transient-suffix transient-suffix-49c84c48> #<transient-suffix transient-suffix-49c84c84> #<transient-suffix transient-suffix-49c84cb0> #<transient-suffix transient-suffix-49c84d1e> #<transient-suffix transient-suffix-49c84d4a>) :if #f(compiled-function (obj) #<bytecode -0x1adb69aa9167e36>))
  cl-find(nil (#<transient-suffix transient-suffix-45c3bac0> #<transient-option transient-option-45c3bafc> #<transient-option transient-option-49c84a12> #<transient-suffix transient-suffix-49c84b12> #<transient-suffix transient-suffix-49c84b4e> #<transient-suffix transient-suffix-49c84b7a> #<transient-suffix transient-suffix-49c84bb6> #<transient-suffix transient-suffix-49c84be2> #<transient-suffix transient-suffix-49c84c48> #<transient-suffix transient-suffix-49c84c84> #<transient-suffix transient-suffix-49c84cb0> #<transient-suffix transient-suffix-49c84d1e> #<transient-suffix transient-suffix-49c84d4a>) :if #f(compiled-function (obj) #<bytecode -0x1adb69aa9167e36>))
  cl-find-if(#f(compiled-function (obj) #<bytecode -0x1adb69aa9167e36>) (#<transient-suffix transient-suffix-45c3bac0> #<transient-option transient-option-45c3bafc> #<transient-option transient-option-49c84a12> #<transient-suffix transient-suffix-49c84b12> #<transient-suffix transient-suffix-49c84b4e> #<transient-suffix transient-suffix-49c84b7a> #<transient-suffix transient-suffix-49c84bb6> #<transient-suffix transient-suffix-49c84be2> #<transient-suffix transient-suffix-49c84c48> #<transient-suffix transient-suffix-49c84c84> #<transient-suffix transient-suffix-49c84cb0> #<transient-suffix transient-suffix-49c84d1e> #<transient-suffix transient-suffix-49c84d4a>))
  #f(compiled-function (cl--cnm obj value) #<bytecode -0xb7805d19ca44f59>)(#f(compiled-function (&rest args) #<bytecode 0x15dbd01ea76037c4>) #<transient-option transient-option-45c3bafc> "a value")
  apply(#f(compiled-function (cl--cnm obj value) #<bytecode -0xb7805d19ca44f59>) #f(compiled-function (&rest args) #<bytecode 0x15dbd01ea76037c4>) (#<transient-option transient-option-45c3bafc> "a value"))
  #f(compiled-function (obj value) "Unset incompatible infix arguments." #<bytecode -0x1625e5fe423227ab>)(#<transient-option transient-option-45c3bafc> "a value")
  apply(#f(compiled-function (obj value) "Unset incompatible infix arguments." #<bytecode -0x1625e5fe423227ab>) #<transient-option transient-option-45c3bafc> "a value")
  transient-infix-set(#<transient-option transient-option-45c3bafc> "a value")
  (let ((obj (transient-suffix-object))) (transient-infix-set obj (transient-infix-read obj)))
  (lambda nil (interactive) (let ((obj (transient-suffix-object))) (transient-infix-set obj (transient-infix-read obj))) (transient--show))()
  funcall-interactively((lambda nil (interactive) (let ((obj (transient-suffix-object))) (transient-infix-set obj (transient-infix-read obj))) (transient--show)))
  command-execute((lambda nil (interactive) (let ((obj (transient-suffix-object))) (transient-infix-set obj (transient-infix-read obj))) (transient--show)))

Please also provide example code so I can quickly try it out. Thanks!

I can see in the screenshot that you have already written it. 😁

Derp I added the example code in the issue description above, sorry about that! I also fixed some of the code links.

Here's a simple EIEIO example demonstrating that slot-boundp will signal an error if the slot does not exist, whereas slot-exists-p will not:

(require 'eieio)

(defclass person () 
  ((name :initarg :name)))

(setq pers (person :name "Eric"))

(slot-boundp pers 'argument) ;;; signals (invalid-slot-name "#<person person-491b5bfc>" argument)

(slot-exists-p pers 'argument) ;; nil (no error)

I'm using Emacs 29 pretest 2. I'm wondering if there were changes to eieio.

I can "fix" the problem by moving the suffix with no argument to the end:

(transient-define-prefix incompatible-bug ()
  :incompatible '(("a=" "b="))
  [""
   ("a" "a" "a=")
   ("b" "b" "b=")
   ("x" "exit" simple-suffix)])

The works around the issue because the cl-find-if call in transient-infix-set will return early.

Very nice analysis!

I can "fix" the problem by moving the suffix with no argument to the end:

Ah, I was confused why the uses in magit seemed to work just fine. Good thing I asked for code, because I would have added the suffix at the end to begin with.

One option would be to replace slot-boundp with slot-exists-p in transient.

That sounds reasonable to me, but let's use both. I was going to say that doing that would be overkill because if a transient-infix's argument slot were unbound, that would be a bug. But a derived class that allows that is actually imaginable, so let's play it safe.

Could you please open a pull-request to fix this?

I'd be happy to open a pull-request!

but let's use both

Can you elaborate? I'm not quite following. How would we use both? Thanks!

Well (and (slot-exists-p ...) (slot-boundp ...) (oref ...)). For transient-infix and all currently existing derived classes, it would be a bug if argument somehow ended up being unbound. So we might just skip slot-boundp. However it is conceivable, that in the future there will be a class that derives from transient-infix (and thus inherits the unbound argument slot), but which does not actually use the argument slot at all and thus leaves it unbound. That might not be very likely and if someone implemented it like that I would likely recommend not inheriting from transient-infix. But we cannot be sure, and thus I think we should play it safe.

Thanks - I will be away for a day but will digest and look into this in a day or two. I may have further questions. I appreciate the quick response and pointers!

I created a pull request: #248