Repository: effekt-lang/effekt (scala)

Issue:
Language server crash on `"initializationOptions": null`
When using the language server via other editors than VSCode (Emacs, neovim, ...), if one has `initializationOptions` set to `null` (which the LSP spec allows, I think), the server crashes in LSP4J.

```
~> 
java.lang.IllegalStateException: Not a JSON Object: null
    at com.google.gson.JsonElement.getAsJsonObject(JsonElement.java:165)
```

<details>
  <summary>Trace</summary>

```

[Trace - 09:38:33 AM] Sending request 'initialize - (3)'.
Params: {
  "processId": 2070,
  "rootPath": "/$USER/prague-tutorial",
  "clientInfo": {
    "name": "emacs",
    "version": "GNU Emacs 30.1 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.49, cairo version 1.18.2)"
  },
  "rootUri": "file:///$USER/prague-tutorial",
  "capabilities": {
    "general": {
      "positionEncodings": [
        "utf-32",
        "utf-16"
      ]
    },
    "workspace": {
      "workspaceEdit": {
        "documentChanges": true,
        "resourceOperations": [
          "create",
          "rename",
          "delete"
        ]
      },
      "applyEdit": true,
      "symbol": {
        "symbolKind": {
          "valueSet": [
            1,
            2,
            3,
            4,
            5,
            6,
            7,
            8,
            9,
            10,
            11,
            12,
            13,
            14,
            15,
            16,
            17,
            18,
            19,
            20,
            21,
            22,
            23,
            24,
            25,
            26
          ]
        }
      },
      "executeCommand": {
        "dynamicRegistration": false
      },
      "didChangeWatchedFiles": {
        "dynamicRegistration": true
      },
      "workspaceFolders": true,
      "configuration": true,
      "codeLens": {
        "refreshSupport": true
      },
      "diagnostics": {
        "refreshSupport": false
      },
      "fileOperations": {
        "didCreate": false,
        "willCreate": false,
        "didRename": true,
        "willRename": true,
        "didDelete": false,
        "willDelete": false
      }
    },
    "textDocument": {
      "declaration": {
        "dynamicRegistration": true,
        "linkSupport": true
      },
      "definition": {
        "dynamicRegistration": true,
        "linkSupport": true
      },
      "references": {
        "dynamicRegistration": true
      },
      "implementation": {
        "dynamicRegistration": true,
        "linkSupport": true
      },
      "typeDefinition": {
        "dynamicRegistration": true,
        "linkSupport": true
      },
      "synchronization": {
        "willSave": true,
        "didSave": true,
        "willSaveWaitUntil": true
      },
      "documentSymbol": {
        "symbolKind": {
          "valueSet": [
            1,
            2,
            3,
            4,
            5,
            6,
            7,
            8,
            9,
            10,
            11,
            12,
            13,
            14,
            15,
            16,
            17,
            18,
            19,
            20,
            21,
            22,
            23,
            24,
            25,
            26
          ]
        },
        "hierarchicalDocumentSymbolSupport": true
      },
      "formatting": {
        "dynamicRegistration": true
      },
      "rangeFormatting": {
        "dynamicRegistration": true
      },
      "onTypeFormatting": {
        "dynamicRegistration": true
      },
      "rename": {
        "dynamicRegistration": true,
        "prepareSupport": true
      },
      "codeAction": {
        "dynamicRegistration": true,
        "isPreferredSupport": true,
        "codeActionLiteralSupport": {
          "codeActionKind": {
            "valueSet": [
              "",
              "quickfix",
              "refactor",
              "refactor.extract",
              "refactor.inline",
              "refactor.rewrite",
              "source",
              "source.organizeImports"
            ]
          }
        },
        "resolveSupport": {
          "properties": [
            "edit",
            "command"
          ]
        },
        "dataSupport": true
      },
      "completion": {
        "completionItem": {
          "snippetSupport": false,
          "documentationFormat": [
            "markdown",
            "plaintext"
          ],
          "resolveAdditionalTextEditsSupport": true,
          "insertReplaceSupport": true,
          "deprecatedSupport": true,
          "resolveSupport": {
            "properties": [
              "documentation",
              "detail",
              "additionalTextEdits",
              "command",
              "insertTextFormat",
              "insertTextMode"
            ]
          },
          "insertTextModeSupport": {
            "valueSet": [
              1,
              2
            ]
          }
        },
        "contextSupport": true,
        "dynamicRegistration": true
      },
      "signatureHelp": {
        "signatureInformation": {
          "parameterInformation": {
            "labelOffsetSupport": true
          }
        },
        "dynamicRegistration": true
      },
      "documentLink": {
        "dynamicRegistration": true,
        "tooltipSupport": true
      },
      "hover": {
        "contentFormat": [
          "markdown",
          "plaintext"
        ],
        "dynamicRegistration": true
      },
      "foldingRange": {
        "dynamicRegistration": true
      },
      "selectionRange": {
        "dynamicRegistration": true
      },
      "callHierarchy": {
        "dynamicRegistration": false
      },
      "typeHierarchy": {
        "dynamicRegistration": true
      },
      "publishDiagnostics": {
        "relatedInformation": true,
        "tagSupport": {
          "valueSet": [
            1,
            2
          ]
        },
        "versionSupport": true
      },
      "diagnostic": {
        "dynamicRegistration": false,
        "relatedDocumentSupport": false
      },
      "linkedEditingRange": {
        "dynamicRegistration": true
      }
    },
    "window": {
      "workDoneProgress": true,
      "showDocument": {
        "support": true
      }
    }
  },
  "initializationOptions": null,
  "workDoneToken": "1"
}


[Trace - 09:38:34 AM] Received response 'initialize - (3)' in 371ms.
Result: {
  "code": -32603,
  "message": "Internal error.",
  "data": "java.lang.IllegalStateException: Not a JSON Object: null\n\tat com.google.gson.JsonElement.getAsJsonObject(JsonElement.java:165)\n\tat effekt.Server.didChangeConfiguration(Server.scala:508)\n\tat effekt.Server.initialize(Server.scala:85)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:580)\n\tat org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.lambda$recursiveFindRpcMethods$0(GenericEndpoint.java:65)\n\tat org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.request(GenericEndpoint.java:128)\n\tat org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.handleRequest(RemoteEndpoint.java:271)\n\tat org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.consume(RemoteEndpoint.java:201)\n\tat org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.handleMessage(StreamMessageProducer.java:185)\n\tat org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.listen(StreamMessageProducer.java:97)\n\tat org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor.run(ConcurrentMessageProcessor.java:114)\n\tat java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)\n\tat java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)\n\tat java.base/java.lang.Thread.run(Thread.java:1583)\n"
}
```

</details>

Pull request description:
Fixes #1041.

Patch:
diff --git a/effekt/jvm/src/main/scala/effekt/Server.scala b/effekt/jvm/src/main/scala/effekt/Server.scala
index 3662a81fd..242177d3d 100644
--- a/effekt/jvm/src/main/scala/effekt/Server.scala
+++ b/effekt/jvm/src/main/scala/effekt/Server.scala
@@ -505,10 +505,15 @@ class Server(config: EffektConfig, compileOnChange: Boolean=false) extends Langu
     // nested under an "effekt" key `{ "effekt": { "showIR": "core", ... } }`
     // The former is sent by the VSCode extension for `initializationOptions`,
     // the latter by newer extension versions for `workspace/didChangeConfiguration`.
-    val newSettings = params.getSettings.asInstanceOf[JsonElement].getAsJsonObject
-    this.settings = newSettings;
-    if (newSettings == null) return
-    val effektSection = newSettings.get("effekt")
+    val newSettings = params.getSettings.asInstanceOf[JsonElement]
+    // When the settings come via `initializationOptions`, they can be null as per the LSP spec.
+    if (newSettings.isJsonNull) {
+      this.settings = null;
+      return;
+    }
+    val newSettingsObj = newSettings.getAsJsonObject
+    this.settings = newSettingsObj;
+    val effektSection = newSettingsObj.get("effekt")
     if (effektSection != null) {
       this.settings = effektSection
     }
@@ -521,18 +526,16 @@ class Server(config: EffektConfig, compileOnChange: Boolean=false) extends Langu
   //
 
   def settingBool(name: String): Boolean = {
-    if (settings == null) return false
+    if (settings == null || settings.isJsonNull) return false
     val obj = settings.getAsJsonObject
-    if (obj == null) return false
     val value = obj.get(name)
     if (value == null) return false
     value.getAsBoolean
   }
 
   def settingString(name: String): Option[String] = {
-    if (settings == null) return None
+    if (settings == null || settings.isJsonNull) return None
     val obj = settings.getAsJsonObject
-    if (obj == null) return None
     val value = obj.get(name)
     if (value == null) return None
     Some(value.getAsString)


Test patch:
diff --git a/effekt/jvm/src/test/scala/effekt/LSPTests.scala b/effekt/jvm/src/test/scala/effekt/LSPTests.scala
index c1234abc9..841ab9418 100644
--- a/effekt/jvm/src/test/scala/effekt/LSPTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/LSPTests.scala
@@ -102,6 +102,15 @@ class LSPTests extends FunSuite {
     }
   }
 
+  // Regression test: https://github.com/effekt-lang/effekt/issues/1041
+  test("Initialization with initializationOptions set to null does not crash") {
+    withClientAndServer { (client, server) =>
+      val params = new InitializeParams()
+      params.setInitializationOptions(JsonParser.parseString("null"))
+      server.initialize(params).get()
+    }
+  }
+
   test("didOpen yields empty diagnostics") {
     withClientAndServer { (client, server) =>
       val didOpenParams = new DidOpenTextDocumentParams()