rescript-vscode icon indicating copy to clipboard operation
rescript-vscode copied to clipboard

Integrate reanalyze into the LSP so more editors can benefit out-of-the-box

Open Josef-Thorne-A opened this issue 4 months ago • 5 comments

Similar issue to: https://github.com/rescript-lang/rescript-vscode/issues/1108

My understanding from reading forums is that currently the excellent reanalyze tool is not integrated into the lsp, but is a separate tool that can either be invoked directly by the vscode plugin or from the rescript-tools cli.

If this could be integrated into the LSP, users like myself who don't use vscode could directly benefit from these tools without having to use vscode. I personally use Emacs, but lsp seems to be the best way to ensure cross-editor compatibility.

I could also work on parsing the output of the cli tool in the emacs mode but this could be a little cumbersome/error prone.

Happy to look into this myself is someone points me in the right direction.

Josef-Thorne-A avatar Sep 01 '25 13:09 Josef-Thorne-A

This should definitely be done. At a glance it feels like it shouldn't be too hard. I'll have a quick look and get back with details soon.

zth avatar Sep 01 '25 13:09 zth

has any progress been made on this? it's a huge missing piece of the puzzle for us non vsc users.

tx46 avatar Dec 14 '25 03:12 tx46

also open to pointers to start the work off

tx46 avatar Dec 14 '25 03:12 tx46

A small clarification + a concrete MVP path (starting from the compiler repo, per the ask):

Where reanalyze “lives” today (compiler repo):

  • reanalyze is implemented in the ReScript compiler repo under analysis/reanalyze/src/ (OCaml).
  • It’s exposed via the existing rescript-editor-analysis executable as a subcommand:
    • rescript-editor-analysis reanalyze ...
  • The -json flag already produces a machine-friendly JSON array including file + 0-based range + severity + message (very close to LSP diagnostics).

How it’s shipped (so non-VSCode editors can use it):

  • rescript-editor-analysis.exe is already bundled in the rescript npm install via @rescript/<platform>/bin/rescript-editor-analysis.exe.
  • So we don’t need a separate “reanalyze binary” distribution for the LSP. The LSP can reuse what the project already has installed.

Where the LSP lives / what’s missing:

  • The editor-agnostic LSP is @rescript/language-server (this repo’s server/ folder).
  • The LSP already has infra to locate and run rescript-editor-analysis for hover/defs/etc (it spawns the binary and parses JSON results).
  • What’s missing is an LSP entry point to trigger reanalyze, plus “diagnostics merging” so reanalyze diagnostics don’t get overwritten by compiler/syntax publishes.

Minimal MVP (editor-agnostic):

  1. Add an LSP command via standard workspace/executeCommand, e.g.:
    • command id: rescript.reanalyze.run
    • args: optionally { projectRootUri } (or infer from active document/workspace folder)
  2. Handler implementation:
    • spawn: rescript-editor-analysis reanalyze -config -json (cwd = project root)
    • parse JSON output items → map to textDocument/publishDiagnostics:
      • severity: warning/error
      • range: provided by reanalyze (already 0-based)
      • message: provided
      • source: "reanalyze"
  3. Store per-file reanalyze diagnostics and merge them with existing compiler/syntax diagnostics before publishing, so they persist until the next reanalyze run (and can be cleared by re-running or by a rescript.reanalyze.clear command).

That would make this usable from Emacs/Neovim immediately (any LSP client can call workspace/executeCommand), without adding VS Code–specific glue and without redesigning reanalyze itself.

cristianoc avatar Dec 14 '25 06:12 cristianoc

Follow-on with concrete file/function touch points inside @rescript/language-server (this repo’s server/ dir), based on how it already spawns rescript-editor-analysis today:

Where to implement the new LSP command

1) Advertise the command in initialize capabilities

File: server/src/server.ts

  • In the initialize handler, add:
    • executeCommandProvider: { commands: ["rescript.reanalyze.run", "rescript.reanalyze.clear"] }

This is the standard LSP way for clients (Emacs/Neovim/etc) to discover server commands.

2) Actually handle workspace/executeCommand

File: server/src/server.ts

  • In the request dispatch inside onMessage (the big else if (p.Message.isRequest(msg)) { ... } chain), add a branch for:
    • p.ExecuteCommandRequest.method (workspace/executeCommand)

Implementation sketch:

  • Parse params as p.ExecuteCommandParams:
    • command string
    • optional arguments
  • Support:
    • rescript.reanalyze.run → run reanalyze and publish diagnostics
    • rescript.reanalyze.clear → clear stored reanalyze diags and republish merged diags (compiler/syntax only)

How to run reanalyze (reuse existing binary discovery + exec infra)

File: server/src/utils.ts

  • There is already runAnalysisAfterSanityCheck(filePath, args, ...) which:
    • picks the right rescript-editor-analysis binary (project.editorAnalysisLocation vs legacy builtinBinaryPath)
    • runs childProcess.execFileSync(binaryPath, args, { cwd: projectRoot, env: ... })
    • JSON-parses stdout
  • However reanalyze is project-wide (not “per file”), so it’s better to add a dedicated helper that:
    1. chooses projectRootPath (from arg or active doc)
    2. resolves binaryPath the same way runAnalysisAfterSanityCheck does
    3. runs: execFileSync(binaryPath, ["reanalyze", "-config", "-json"], { cwd: projectRootPath, ...env })
    4. parses the JSON array (reanalyze’s -json output already contains file + range + kind + message + name)

I’d put this in utils.ts as e.g. runReanalyzeForProject(projectRootPath): Promise<ReanalyzeItem[]>.

Where to store and merge diagnostics (so they don’t get overwritten)

Key point: LSP textDocument/publishDiagnostics is per file and replaces the entire diagnostic set for that URI. Today the server publishes:

  • compiler diagnostics in sendUpdatedDiagnostics() (reads .compiler.log, publishes for many files)
  • syntax diagnostics for open files in updateDiagnosticSyntax() (merges syntax + compiler for that file)

So if we publish reanalyze-only diags once, the next compiler-log publish will overwrite them unless we merge them.

3) Add storage for reanalyze diagnostics per project

File: server/src/projectFiles.ts

  • Extend projectFiles with:
    • reanalyzeDiagnostics: filesDiagnostics (keyed by file URI)

4) Merge reanalyze diags in BOTH publishing paths

File: server/src/server.ts

A) In sendUpdatedDiagnostics():

  • Today it sets projectFile.filesDiagnostics = filesAndErrors and then publishes filesAndErrors[fileUri].
  • Change it to publish:
    • merged = (filesAndErrors[fileUri] ?? []) + (projectFile.reanalyzeDiagnostics[fileUri] ?? [])

B) In updateDiagnosticSyntax(fileUri, fileContent):

  • Today it merges: syntaxDiagnosticsForFile + compilerDiagnosticsForFile
  • Change it to also add: reanalyzeDiagnosticsForFile from the current project’s reanalyzeDiagnostics[fileUri].

Mapping details (reanalyze JSON → LSP Diagnostic)

  • item.kind ("warning"/"error") → Diagnostic.severity (Warning/Error)
  • item.range [sl, sc, el, ec] → Diagnostic.range
  • item.messageDiagnostic.message
  • item.nameDiagnostic.code (optional but useful)
  • Diagnostic.source = "reanalyze"

cristianoc avatar Dec 14 '25 06:12 cristianoc