A comprehensive Language Server Protocol implementation for the ucode scripting language. It provides flow-sensitive type inference, target-version-aware diagnostics, autocompletion of builtins and module members, go-to-definition and hover across files, quick fixes, and a standalone CLI checker — for VS Code, Neovim, and any LSP-capable editor.
Both plain scripts (.uc) and ucode templates (.ut, with embedded-ucode syntax highlighting) are supported, including full awareness of LuCI's ucode runtime — see LuCI & OpenWrt awareness.
Every diagnostic carries a stable UC#### code so you can look it up, filter it, or suppress it.
The LSP infers types flow-sensitively, tracking how a value's type changes as it flows through assignments, guards, and branches. Types are unions (integer | string | null), so you can see exactly what you're working with — including the cases you haven't handled yet — before you deploy.
ucode is a dynamically typed language, so the type system is deliberately not total: it cannot prove the type of every expression, and it does not try to. Two language realities drive this, and the LSP gives you a way to address each one:
-
Static gaps — what the checker can't see. Function parameters, values that cross module boundaries, and data read from the outside world have no statically knowable type. Rather than guess, the LSP lets you annotate intent with JSDoc-style type comments (
@param,@returns,@typedef). These feed directly into inference, so an annotated parameter or return value propagates everywhere it's used. A quick fix can generate a/** @param */block with types inferred from how the parameter is used in the body, and@returnsis reconciled against the inferred return type. Runucode-lsp --help-typesfor the annotation guide. -
Runtime gaps — what can fail at runtime. Many builtins legitimately return
T | null(e.g.fs.open()→fs.file | null,fs.readfile()→string | null). The checker surfaces these as nullable unions and then respects the narrowing you write: a null guard (if (fh) …), atype()orexistscheck, or optional chaining (fh?.read()) narrowsT | nulldown toTfor the rest of that flow. Member access on a still-nullable value is flagged (a warning, escalating to an error under'use strict';), pointing you at the runtime check that's actually missing.
In short: inference does as much as is soundly possible, JSDoc annotations close the static gaps, and runtime checks close the nullable-return gaps — and the checker rewards both.
- Null safety — member access on a provably-null value is an error, and access on a possibly-null
T | nullvalue is a warning that escalates to an error under'use strict';. - Builtin call validation — argument counts, types, and coercions checked against real ucode signatures (including a full
printf/sprintfformat checker), with precise line/column positions. No more vague "left-hand side is not a function" errors. - Scope analysis — undefined variables,
constreassignment, shadowing, use-before-declaration, and unused imports, with ucode's non-strict vs'use strict';semantics modeled faithfully (implicit globals, last-write-wins redeclaration, etc.). - Target-version awareness — modules, functions, and methods are gated to a chosen OpenWrt/ucode release (
UC6005); using something newer than your target is flagged. See Target version. - Suppression directives —
// ucode-lsp disable/disable-next-line, optionally code-targeted (disable UC1001 UC1006); in template text, use the comment form{# ucode-lsp disable-next-line UC6020 #}. The directives are themselves checked: a stale one (suppressing nothing) is flagged with a removal fix, and a used blanket disable gets a faded hint listing the exact codes it suppresses, with a one-click fix to narrow it to them.
- Add a missing import for an unresolved module member.
- Add inferred null guards or optional chaining (
?.) on possibly-null access. - Insert a
/** @param */JSDoc block with types inferred from body usage. - Mark a parameter optional (
@param {string} [body]) when call sites omit it — one edit at the declaration clears every call-site diagnostic. - Coerce a non-string argument where a builtin expects one.
- Fix a typo'd
luci.*import to the module the tree actually ships ("did you mean…"). - Repair an over-long
{# … #}template comment (a#}inside the body ends it early — the fix splits the inner pairs so the comment reaches its real terminator). - Remove or narrow suppression directives (see above).
- Type-guard narrowing fixes, generated from the AST (not text scraping).
- Context-aware completions for builtins, locals, and module members.
- Module-specific completions, e.g.
fs.→open,readfile,writefile, … - Member completion on object values, including
this., optional chaining (obj?.), nested members, and namespace constants (nl80211.const.). - Completion is correctly suppressed inside strings and comments.
- Go to Definition across files, following re-export chains.
- Find References and Rename (workspace-wide).
- Hover showing inferred types and function signatures — including on import-source strings (
from 'lucihttp'shows the module's documentation and availability;from './helper.uc'shows the resolved file and its exports). - Signature help with parameter info as you type a call.
- Document symbols, folding ranges, document highlights, and inlay hints.
- Code lens — Git history and reference count above each function (VS Code).
npm install -g ucode-lspThis gives you the ucode-lsp command with two modes:
Check mode — scan files and print diagnostics (like tsc):
ucode-lsp # check all .uc files in current directory
ucode-lsp src/ # check a specific directory
ucode-lsp file.uc # check a specific file
ucode-lsp --verbose # include info-level diagnosticsLSP server mode — for editors:
ucode-lsp --stdio # start LSP server over stdioRun ucode-lsp --help for all options, or ucode-lsp --help-types for the type annotation guide. A man page is also available: man ucode-lsp.
Install the extension from the VS Code Marketplace.
Add to ~/.config/nvim/init.lua:
vim.filetype.add({ extension = { uc = 'ucode', ut = 'ucode' } })
vim.lsp.config('ucode', {
cmd = { 'ucode-lsp', '--stdio' },
filetypes = { 'ucode' },
root_markers = { '.git' },
})
vim.lsp.enable('ucode')(.ut templates are detected by file extension server-side, so mapping them to the same filetype is enough.)
git clone https://github.com/NoahBPeterson/ucode-lsp.git
cd ucode-lsp
bun install && bun run compile
npm install -g . # install CLI globally from local buildThis produces:
dist/server.js— LSP serverdist/cli.js— CLI checkerbin/ucode-lsp.js— entry point (routes to server or CLI)
split("hello", 123); // Error: split() expects (string, string)
length(42); // Error: length() expects a string, array, or object
let x = 5;
function test() {
print(y); // Error: 'y' is not defined ('use strict')
let x = 10; // Warning: shadows outer 'x'
let e = open(); // Error: Undefined function 'open'
};
const PORT = 80;
PORT = 443; // Error: assignment to constant 'PORT' (UC1010)import { open } from 'fs';
let fh = open("data.txt", "r"); // fs.file | null
fh.read("all"); // Warning: 'fh' is possibly null
fh?.read("all"); // ok — optional chainingComplete IntelliSense for built-in modules:
import { create, connect, AF_INET, SOCK_STREAM } from 'socket';
import * as math from 'math';
import { query } from 'resolv';
const fs = require('fs');
let sock = create(AF_INET, SOCK_STREAM); // ✅ Full autocomplete
let result = connect(sock, "example.com", "80");
let sqrt_val = math.sqrt(16); // ✅ Namespace imports
let file = fs.open("test.txt", "r"); // ✅ fs.open(), fs.readfile(), fs.writefile()...
let content = fs.readfile("data.txt"); // string | nullSupported modules (all typed, completed, and gated to your target version by first feed availability, ground-truthed against real per-release OpenWrt images):
- bpf — eBPF program/map access (23.05+)
- debug — runtime debugging and introspection (23.05+)
- digest — cryptographic hash functions (24.10+)
- fs — file system operations (
open,readfile,writefile,stat, …) - html — HTML entity encoding and tag stripping (23.05+)
- io — I/O handle operations (25.12+)
- log — system logging (syslog, ulog functions) (23.05+)
- lua — Lua interpreter bridge (23.05+)
- lucihttp — LuCI HTTP utility library: URL percent-encoding, urlencoded/multipart body parsers
- math — mathematical functions (
sin,cos,sqrt, …) - nl80211 — WiFi/802.11 networking
- pkgen — package generation helpers (25.12+)
- resolv — DNS resolution
- rtnl — netlink routing (routes, links, addresses)
- socket — network socket functionality (24.10+)
- struct — binary data packing/unpacking
- ubus — OpenWrt inter-process communication
- uci — OpenWrt UCI configuration management
- uclient — HTTP/FTP client transfers (24.10+)
- udebug — OpenWrt debug ring buffers (24.10+)
- uline — line-editing / readline (25.12+)
- uloop — event loop and timer functionality
- zlib — compression/decompression (25.12+)
Beyond importable modules, the LSP also models host-injected runtime objects — the globals C daemons bind into their embedded ucode VMs, seeded only in the matching file context: uhttpd request handlers, netifd proto/daemon scripts, hostapd/wpas scripts, and the LuCI dispatcher environment below.
LuCI's ucode runtime does things no file-local analysis can see — so the LSP models the runtime itself, activating on evidence (never guesswork) in three contexts: a LuCI source checkout, a standalone package repo (a Makefile with LUCI_TITLE/luci.mk, the convention of real third-party apps and themes), or an extracted device rootfs (/usr/share/ucode/luci/).
.uttemplates are a first-class language: wrapper grammar with embedded-ucode highlighting for{% %}/{{ }}blocks and{# #}comments, plus full analysis inside the blocks.- The dispatcher environment is ambient in templates and controllers:
http,ubus,uci,dispatcher,ctx,theme,media,_(),entityencode(), … — all typed with real signatures (hoverhttp.formvaluefor its actual contract; go-to-definition lands in luci-base's own source). - Template-root includes:
include('header')resolves against the tree'sucode/template/roots the wayrender_anyreally works — not file-relative — including theme-dispatch patterns likeinclude(`themes/${theme}/header`). Missing templates with a Lua-view fallback (luasrc/view/*.htm) are not flagged. - Render scopes travel across files: variables passed via
include('page', scope)/runtime.render(...)are known inside the target template — mined through object literals, local bindings, and callback indirection — so{{ exitcode }}in a template hovers asinteger, traced back to the controller that supplied it. luci.*modules resolve against the tree (every package'sucode/dir mirrors/usr/share/ucode/luci/on-device); unresolvable ones that the device provides (compiledluci.core, generatedluci.version, other packages' modules) stay silent instead of false-positive — while a typo of a module the tree itself ships gets a did-you-mean with a one-click fix.- Everything above holds for out-of-tree development: real-world repos like luci-theme-argon and luci-app-podman analyze with zero false "undefined" warnings, validated against 14 published third-party packages and opkg/apk-installed rootfs images of OpenWrt 23.05, 24.10, and 25.12. (On 22.03, which predates the ucode-based LuCI, none of this activates — correctly.)
Diagnostics are gated to a target OpenWrt/ucode release so the LSP only offers — and only accepts — what exists in your deployment target. The default is the latest stable release (25.12).
- VS Code: set
ucode.targetVersion, or run "ucode: Select target OpenWrt/ucode version" from the Command Palette. - Allowed values:
main,25.12,24.10,23.05,22.03.
{
"ucode.targetVersion": "25.12",
"ucode.maxNumberOfProblems": 100,
"ucode.inlayHints.enable": true,
"ucode.trace.server": "off"
}- ucode: Select target OpenWrt/ucode version
- ucode: Show Function Git History
- ucode: Show Function References
- Lexer (
src/lexer/) — tokenization and basic syntax validation - Parser (
src/parser/) — AST generation with error recovery - Semantic analyzer (
src/analysis/) — type inference, flow analysis, and scope checking; module type definitions and the version-gating registry - LSP server (
src/server.ts) — editor integration via LSP (editor-agnostic) - CLI checker (
src/cli.ts) — standalone diagnostic output (liketsc)
- Make changes to source files in
src/. - Build with
bun run compile. - Verify types with
tsc --noEmitand run the test suite. - Test interactively in the VS Code extension host.
- Submit a pull request.
# Primary testing with Bun
bun test tests/
# Node.js fallback for individual files
npx mocha tests/test-real-double-diagnostic-fix.test.js
node tests/specific-test-file.jsMIT License — see LICENSE file for details.