@gdscript-analyzer/core
Native Node.js bindings for gdscript-analyzer — a fast, embeddable GDScript (Godot 4.x) static-analysis library. "Roslyn for Godot."
Analyze GDScript headlessly — no running Godot editor — from Node: diagnostics,
type-aware hover, completion, go-to-definition, find-references, rename, document
& workspace symbols, signature help, folding ranges, inlay hints, and code
actions. This is the native binding (a napi-rs addon, no
WASM overhead); for the browser use @gdscript-analyzer/wasm.
npm i @gdscript-analyzer/corePrebuilt binaries ship for macOS (x64 / arm64), Windows (x64), and Linux (x64 / arm64-gnu); npm installs only the one for your platform.
Quick start
import { AnalysisHandle } from "@gdscript-analyzer/core";
const az = new AnalysisHandle();
const uri = "inmemory://player.gd";
az.openDocument(uri, `extends Node
func _ready() -> void:
var half = 5 / 2 # integer division
print(half)
`, null);
// Diagnostics come back as a JSON string.
console.log(JSON.parse(az.diagnostics(uri)));
// → [{ code: "INTEGER_DIVISION", severity: "warning", range: { start, end }, message: ... }]Every query that returns data returns a JSON string (parse it). Positions are UTF-8 byte offsets (see Positions below).
The session model
AnalysisHandle is a live, URI-keyed session. Construct it once, push
documents, then query — the underlying Rust AnalysisHost (and its incremental
salsa cache) stays warm across edits, so re-queries after a small edit are cheap.
const az = new AnalysisHandle();
// Open / replace / close documents by URI.
az.openDocument(uri, text, resPath); // resPath ("res://…") is optional — see below
az.changeDocument(uri, newText); // replace text (unknown URI ⇒ upsert)
az.closeDocument(uri);
az.isOpen(uri); // → boolean
// Optional project context (enables [autoload] singleton resolution).
az.setProjectConfig(projectGodotText);Cross-file resolution
Pass each document's res:// path on first open to enable cross-file
preload(...), extends "res://…", and autoload resolution:
az.openDocument("inmemory://player.gd", playerSrc, "res://entities/player.gd");
az.openDocument("inmemory://enemy.gd", enemySrc, "res://entities/enemy.gd");
// Now hover/goto across the two files resolves correctly.resPath is recorded once and ignored on later openDocument calls for the same
URI (re-sending would needlessly invalidate the resource-path registry). Use
changeDocument for edits.
API
All queries take a uri; offset-based queries take a UTF-8 byte offset.
Array/object queries return a JSON string; the … | null ones return JS
null when there's nothing at the offset.
| Method | Returns | What |
|---|---|---|
diagnostics(uri) |
JSON array | parse + type diagnostics |
documentSymbols(uri) |
JSON array | the document outline |
foldingRanges(uri) |
JSON array | foldable ranges |
inlayHints(uri) |
JSON array | inferred-type / param inlay hints |
completions(uri, offset) |
JSON array | completions at offset |
hover(uri, offset) |
JSON string | null |
type + docs at offset |
signatureHelp(uri, offset) |
JSON string | null |
active call signature |
codeActions(uri, offset) |
JSON array | quick fixes at offset |
gotoDefinition(uri, offset) |
JSON array | definition target(s) |
findReferences(uri, offset) |
JSON array | all references |
rename(uri, offset, newName) |
JSON string | {"ok": SourceChange} or {"error": RenameError} |
workspaceSymbols(query) |
JSON array | project-wide symbol search |
syntaxTree(uri) |
JSON string | null |
pretty-printed CST (debugging) |
const offset = 38; // a UTF-8 byte offset into the document
JSON.parse(az.completions(uri, offset));
const hover = az.hover(uri, offset); // string | null
if (hover) console.log(JSON.parse(hover));
JSON.parse(az.gotoDefinition(uri, offset));Positions (byte offsets)
The analyzer speaks UTF-8 byte offsets, not line/column or UTF-16 code units. JavaScript strings are UTF-16, so convert at the boundary:
const enc = new TextEncoder();
// UTF-16 index (e.g. from an editor) → UTF-8 byte offset
const byteOffset = enc.encode(text.slice(0, utf16Index)).length;If you're building an LSP/editor integration, the diagnostic/hover/definition
ranges come back as byte offsets too — convert them back to your editor's
position type on the way out.
Notes
- No Godot required. Analysis is fully static; it never launches or talks to a Godot editor.
- Single-threaded ownership. An
AnalysisHandleis owned by the JS thread that created it; don't share one instance across worker threads (create one per thread instead). - Engine model. The Godot 4.x class/method/signal model is embedded in the native binary — no extra asset to load (unlike the wasm package).
Links
- Repository & docs: https://github.com/yanivkalfa/gdscript-analyzer
- Live playground: https://yanivkalfa.github.io/gdscript-analyzer/playground/
- Browser package:
@gdscript-analyzer/wasm - License: MIT OR Apache-2.0