jwiegley / use-package

A use-package declaration for simplifying your .emacs
https://jwiegley.github.io/use-package
GNU General Public License v3.0
4.42k stars 260 forks source link

Byte compilation warning "the function ‘js2-imenu-extras-setup’ might not be defined at runtime." #1032

Open YorkZ opened 1 year ago

YorkZ commented 1 year ago

This simple code:

(use-package js2-imenu-extras
  :functions js2-imenu-extras-setup
  :config
  (js2-imenu-extras-setup))

gives me the byte compilation warning:

the function ‘js2-imenu-extras-setup’ might not be defined at runtime.

I've spent hours but still wasn't able to silence the warning. I'm wondering whether it would be possible to solve it.

redblobgames commented 1 year ago

I always have trouble making :functions work for me. Try M-x pp-macroexpand-expression and you'll see your :functions doesn't seem to show up anywhere.

Other people have issues too, and you might try some of the suggestions in these issues:

AlynxZhou commented 1 year ago

I can reproduce this, too, using :commands instead of :functions works, but they have different meanings.

AlynxZhou commented 1 year ago

I see @skangas asking for a minimal reproduce step for this in https://github.com/jwiegley/use-package/issues/636#issuecomment-1342022704, I do not byte-compile my init file, but I got those warnings from flycheck, and I'd like to show how to reproduce it:

  1. Install flycheck, rainbow-mode (or other package, I use this as example) and hook flycheck-mode with emacs-lisp-mode-hook so it will run.
  2. Create a file called test.el with following content and save it under your ~/.emacs.d/ (!!!IMPORTANT!!!):
(use-package rainbow-mode
  :functions (rainbow-x-color-luminance)
  :config
  (rainbow-x-color-luminance "cyan"))
  1. M-x flycheck-compile RET emacs-lisp RET

You should get a warning about the function ‘rainbow-x-color-luminance’ might not be defined at runtime., and it's not expected, I already define it with :functions.

I also did further investigetion, first this only happens when a file is under ~/.emacs.d/, otherwise you just get an error about Cannot load rainbow-mode and it will skip this code block.

I also tried to expand the macro, but in compiling state with the following hack:

  1. M-: (setq byte-compile-current-file (current-buffer)) RET
  2. M-x pp-macroexpand-last-sexp RET

Then I got the following result:

(progn
  (eval-and-compile
    (declare-function rainbow-x-color-luminance "rainbow-mode")
    (eval-when-compile
      (with-demoted-errors "Cannot load rainbow-mode: %S" nil
               (unless
                   (featurep 'rainbow-mode)
                 (load "rainbow-mode" nil t)))))
  (defvar use-package--warning67
    #'(lambda
    (keyword err)
    (let
        ((msg
          (format "%s/%s: %s" 'rainbow-mode keyword
              (error-message-string err))))
      (display-warning 'use-package msg :error))))
  (condition-case-unless-debug err
      (if
      (not
       (require 'rainbow-mode nil t))
      (display-warning 'use-package
               (format "Cannot load %s" 'rainbow-mode)
               :error)
    (condition-case-unless-debug err
        (progn
          (rainbow-x-color-luminance "cyan")
          t)
      (error
       (funcall use-package--warning67 :config err))))
    (error
     (funcall use-package--warning67 :catch err))))

So use-package do declare functions in compiling. But if I save those results in another file under ~/.emacs.d/, and do M-x flycheck-compile RET emacs-lisp RET, I got the same warning (the function ‘rainbow-x-color-luminance’ might not be defined at runtime.).

So the problem is: we declared the function, but seems not help.

Hoping those infomation helpful to you!

YorkZ commented 1 year ago

Thanks @AlynxZhou for sharing. Your trick of setting byte-compile-current-file to (current-buffer) before expanding the macro is really interesting. I never knew this before, and each time I expanded the use-package macro that specified :functions FUNCTION , the expanded code never has the declaration of the FUNCTION. Can you explain the mechanism behind this?

AlynxZhou commented 1 year ago

Thanks @AlynxZhou for sharing. Your trick of setting byte-compile-current-file to (current-buffer) before expanding the macro is really interesting. I never knew this before, and each time I expanded the use-package macro that specified :functions FUNCTION , the expanded code never has the declaration of the FUNCTION. Can you explain the mechanism behind this?

See https://github.com/jwiegley/use-package/blob/master/use-package-core.el#L658-L674.

Those keywords only exist to make byte-compiler happy, so use-package only expands them when byte-compiling, if you expand it normally you won't get them. And the actual problem is use-package do declare functions but byte-compiler still gives warnings.

jyp commented 1 year ago

The use-package documentation states:

Normally, use-package will load each package at compile time before compiling the configuration, to ensure that any necessary symbols are in scope to satisfy the byte-compiler.

It appears that this is false in general and there is an error in use-package. Indeed, consider for example:

(use-package js2-imenu-extras
  :defer t
  :config
  (js2-imenu-extras-setup))

This expands to:

(progn
  (eval-and-compile
    (eval-when-compile
      (with-demoted-errors "Cannot load js2-imenu-extras: %S" nil
                           (unless
                               (featurep 'js2-imenu-extras)
                             (load "js2-imenu-extras" nil t)))))
  (eval-after-load 'js2-imenu-extras
    '(progn
       (js2-imenu-extras-setup)
       t)))

The call (load "js2-imenu-extras" nil t) is indeed there, but it is in the body of eval-when-compile. As far as I understand, this does not cause the byte-compiler to load the required file. For this to work, eval-and-compile should be used. (Don't ask me why). So, the fix appears to be to replace, in use-package-core.el, after line 666 (yes really):

              `((eval-when-compile
                  (with-demoted-errors

by

              `((eval-and-compile
                  (with-demoted-errors

Please confirm that this fixes the problem for you, and perhaps submit a PR.

jyp commented 1 year ago

Unfortunately my suggestion causes some runtime cost--- so it's not suitable. For reference, with eval-when-compile my init time is abound 0.35 seconds, but it goes to 0.70 seconds with eval-and-compile. This is when init.el is interpreted. If I try to compile it, the init time goes to 2.7 seconds.

jyp commented 1 year ago

I've kept researching the issue, and I found that this variant is better:

(defmacro jyp/eval-when-compile (&rest body)
    "Evaluate BODY at compile time.
When expanding the macro, evaluate (progn BODY) and substitute
the macro call by the result of the evaluation.  If BODY
is (require feature), then all the symbols declared in feature
are visible to the compiler. This contrasts with
`eval-when-compile', which only puts *variables* in scope."
    (list 'quote (eval (cons 'progn body))))

Using it, init time is 0.7s when init.el is interpreted, and 0.35s when it is compiled. I'm not sure what kind of magic is performed by eval-when-compile. I cannot explain the timings that I'm seeing either.