joaotavora/eglot

Unable to configure `basedpyright`

VictorCMiraldo opened this issue ยท 8 comments

The Problem

I was trying to send some options to basedpyright from eglot but some options don't seem to be respected. The basedpyright.typeCheckingMode works, but basedpyright.analysis.diagnosticSeverityOverrides doesn't, for example.
Said settings do work in lsp-mode with https://github.com/emacs-lsp/lsp-pyright

Based on the server's config description (link), I'd like to send { "basedpyright": { "analysis": { "diagnosticSeverityOverrides": { "reportUnusedCallResult": "none" }}}}, for example, and see the following python file indeed not displaying the warning on line 5:

def even(n: int) -> bool:
    return n % 2 == 0

if __name__ == "__main__":
    even(12)
    x = even(11)
    print(f"{x}")

MRE

init.el for eglot
(require 'use-package)
(package-initialize)

(use-package eglot
  :ensure t
  :config
    (add-to-list 'eglot-server-programs '(
      (python-mode python-ts-mode)
         "basedpyright-langserver" "--stdio"
    ))
    (setq-default eglot-workspace-configuration
      '(:basedpyright (
          ;; Changing this to "off" works, so eglot is communicating something!
          :typeCheckingMode "recommended"
          ;; The rest of the settings are not respected though :(
          :analysis (
            :diagnosticSeverityOverrides (
              :reportUnusedCallResult "none"
            )
            :inlayHints (
              :callArgumentNames :json-false
              :functionReturnTypes :json-false
              :variableTypes :json-false
              :genericTypes :json-false
            )
          )
        )
      )
    )
)
init.el for lsp-mode (this works)
(require 'use-package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)

(use-package f
  :ensure t)

(use-package lsp-mode
  :ensure t)
(use-package lsp-ui
  :ensure t)

(use-package lsp-pyright
  :vc (:url "https://github.com/emacs-lsp/lsp-pyright")
  :custom
    (lsp-pyright-langserver-command "basedpyright") ;; or basedpyright
    (lsp-pyright-diagnostic-severity-overrides '(("reportUnusedCallResult" . "none")))
    (lsp-pyright-type-checking-mode "all")
  :hook (python-mode . (lambda ()
                          (require 'lsp-pyright)
                          (lsp))))  ; or lsp-deferred

I saved both the EGLOT buffer and the lsp-workspace-show-log buffer in the hopes of comparing both and attempting to understand whether I've done something wrong but I fail to see any reason why basedpyright obeys lsp-mode but not eglot.
What's especially suspect is that the typeCheckingMode setting is obeyed on eglot. Am I sending the other settings in the wrong manner? I also tried using the :initializationOptions when adding basedpyright to eglot-server-programs but no success either.

Thank you!

This is what Eglot sent the server based on your configuration.

{
  "jsonrpc": "2.0",
  "method": "workspace/didChangeConfiguration",
  "params": {
    "settings": {
      "basedpyright": {
        "typeCheckingMode": "recommended",
        "analysis": {
          "diagnosticSeverityOverrides": {
            "reportUnusedCallResult": "none"
          },
          "inlayHints": {
            "callArgumentNames": false,
            "functionReturnTypes": false,
            "variableTypes": false,
            "genericTypes": false
          }
        }
      }
    }
  }
}

Then basedpyright asks three times for configuration (why?, idk...) it asks

[jsonrpc] e[14:40:58.932] <-- workspace/configuration[1] {"jsonrpc":"2.0","id":1,"method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///home/victor/test-proj","section":"python"}]}}

To which Eglot replies with [null], because there is no python section the configuration. Then asks

[jsonrpc] e[14:40:58.950] <-- workspace/configuration[2] {"jsonrpc":"2.0","id":2,"method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///home/victor/test-proj","section":"basedpyright.analysis"}]}}

To which Eglot replies with [null, because there is no basedpyright.analysis section the configuration. Then asks

[jsonrpc] e[14:40:58.958] <-- workspace/configuration[3] {"jsonrpc":"2.0","id":3,"method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///home/victor/test-proj","section":"basedpyright"}]}}

To which Eglot replies with:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": [
    {
      "typeCheckingMode": "recommended",
      "analysis": {
        "diagnosticSeverityOverrides": {
          "reportUnusedCallResult": "none"
        },
        "inlayHints": {
          "callArgumentNames": false,
          "functionReturnTypes": false,
          "variableTypes": false,
          "genericTypes": false
        }
      }
    }
  ]
}

Because there is indeed a basedpyright section in the configuration. In fact, Eglot had already communicated it to the server, but maybe it forgot or is hard of hearing or something. Lsp mode replies witht he same large section to all three requests. Maybe basepyright needs to hear the same thing four times to be able to use it.

To which Eglot replies with [null, because there is no basedpyright.analysis section the configuration.

This bit is probably the culprit .

There could be a basedpyright.analysis section in the configuraiton depending on how you interpret the string basedpyright.analysis, which could be "the subsection analysis within the top-level section basedpyright". However the dotted syntax is not specified in https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration and Eglot does not support it.

Maybe something like this is what you should be using:

(setq-default
 eglot-workspace-configuration
 '(:basedpyright
   #1=(:typeCheckingMode
       "recommended"
       :analysis
       #2=(:diagnosticSeverityOverrides
           (:reportUnusedCallResult "none")
           :inlayHints
           (:callArgumentNames :json-false
            :functionReturnTypes :json-false
            :variableTypes :json-false
            :genericTypes :json-false)))
   :basedpyright.analysis #2#
   :python #1#))

Amazing! That does work indeed! It would never have occurred to me that the dot had to be part of the name of a section. I think basedpyright could certainly improve here. It should have listened to the first config sent! I'll summarize your analysis on the basedpyright issue I made.

Thank you so much for the pedagogical analysis, I learned what to look for and how to interpret some of those messages. (And thank you for Eglot, it's great!)

Glad I could help

hi i'm the maintainer of basedpyright. thanks for looking into this. if i'm understanding this correctly, you're saying the issue is that the config is being sent by the client in the following structure:

{
    "basedpyright": {
        "analysis": {
            "diagnosticSeverityOverrides": {
                "reportUnusedCallResult": "none"
            }
        }
    }
}

but the server is expecting it to be:

{
    "basedpyright.analysis": {
        "diagnosticSeverityOverrides": {
            "reportUnusedCallResult": "none"
        }
    }
}

forgive my ignorance, i had no experience working with language servers until i started working on basedpyright, and there have been many issues with pyright's language server that i've had to fix because it was designed with only vscode in mind. vscode settings are defined like this btw, so i assume that's got something to do with it:

// .vscode/settings.json
{
    "basedpyright.analysis.diagnosticSeverityOverrides": {
        "reportUnusedCallResult": "none"
    }
}

that said, i'm confused as to why this issue only seems to have come up in eglot but none of the other supported LSP clients. for example @VictorCMiraldo said lsp-mode doesn't have this issue (DetachHead/basedpyright#894 (comment)), and i can confirm that nvim-lspconfig also works with this struc:ture:

  {
    "neovim/nvim-lspconfig",
    opts = {
      servers = {
        basedpyright = {
          settings = {
            basedpyright = {
              analysis = {
                diagnosticSeverityOverrides = {
                  reportUnusedCallResult = "none",
                }
              }
            },
          },
        },
      },
    },
  }

not trying to blame eglot, i'm sure it's something wrong on my side, i'm just really confused as to what's going on here. i would try to investigate further but i have no idea how to use emacs and i haven't been able to figure out an easy way to debug the language server outside of the vscode extension.

I think I understand the issue, I left my opinion on the basedpyright's repository (DetachHead/basedpyright#894 (comment)) since this is not eglot's fault. Eglot is a law-abiding client... It is the law that is a little too open.

hi i'm the maintainer of basedpyright. thanks for looking into this. if i'm understanding this correctly, you're saying the issue is that the config is being sent by the client in the following structure:

No, that's not it. It's similar, but not exactly that.

not trying to blame eglot,

I know, and it wouldn't be a problem if you did ;-)

There are separate things at play here. In no particular order.

  • Perhaps in contrast to other LSP clients, the Eglot LSP client takes the initiative of sending an early didChangeConfiguration with the full settings configuration section, as illustrated above. Not all clients do this, but it's legal, and some servers like it and it's enough to prime their configurations. basedpyright would seem to ignore it (which, as far as I understand, is legal).

  • Anyway, basedpyright still asks for configuration again, even though Eglot has in practice no more configuration to give it. It asks for three different sections for the same uri: python, basedpyright.analysis and basedpyright (in this order). Eglot replies with the empty [null] response to the first two and with a repeat of what it had already sent to the third one.

  • I'm guessing that Eglot's [null] response to the middle query basedpyright.analysis causes the server to think: "oh, when I asked the client specifically for analysis within basedpyright it told me it has no analysis preferences at all, let me just use the defaults"

  • The interpretation for the dotted syntax basedpyright.analysis is fairly typical, but it is not standardized. I guessed that the meaning was that typical one, so I craft an Eglot configuration snippet for the OP here that would give basedpyright what it wants.

  • If there's anything to change on Eglot's side, it's to make it understand this dotted thing syntax, but that wouldn't be following the standard, just following other clients, which is a bit iffy. There's always the risk of breaking something somewhere on another server.

  • If there's something to change on Basedpyright's side, it's to make it not overwrite (but rather merge) what has given to it via didChangeConfiguration or :initializationOptions

PS: I used Basedpyright from time to time when writing python (with Eglot) Good job making a fork of pyright!