8000
Skip to content

Repository files navigation

ucode Language Server Protocol (LSP)

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.

Type inference

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 @returns is reconciled against the inferred return type. Run ucode-lsp --help-types for 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) …), a type() or exists check, or optional chaining (fh?.read()) narrows T | null down to T for 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.

Features

Diagnostics

  • Null safety — member access on a provably-null value is an error, and access on a possibly-null T | null value 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/sprintf format checker), with precise line/column positions. No more vague "left-hand side is not a function" errors.
  • Scope analysis — undefined variables, const reassignment, 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.

Quick fixes & code actions

  • 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).

Autocompletion

  • 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.

Code navigation & info

  • 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).

Quick Start

npm (CLI + LSP server)

npm install -g ucode-lsp

This 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 diagnostics

LSP server mode — for editors:

ucode-lsp --stdio             # start LSP server over stdio

Run 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.

VS Code

Install the extension from the VS Code Marketplace.

Neovim (0.11+)

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.)

Building from Source

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 build

This produces:

  • dist/server.js — LSP server
  • dist/cli.js — CLI checker
  • bin/ucode-lsp.js — entry point (routes to server or CLI)

Examples

Error detection

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)

Null safety

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 chaining

Module support & autocompletion

Complete 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 | null

Supported 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 & OpenWrt awareness

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/).

  • .ut templates 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 (hover http.formvalue for its actual contract; go-to-definition lands in luci-base's own source).
  • Template-root includes: include('header') resolves against the tree's ucode/template/ roots the way render_any really works — not file-relative — including theme-dispatch patterns like include(`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 as integer, traced back to the controller that supplied it.
  • luci.* modules resolve against the tree (every package's ucode/ dir mirrors /usr/share/ucode/luci/ on-device); unresolvable ones that the device provides (compiled luci.core, generated luci.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.)

Configuration

Target version

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.

VS Code settings

{
  "ucode.targetVersion": "25.12",
  "ucode.maxNumberOfProblems": 100,
  "ucode.inlayHints.enable": true,
  "ucode.trace.server": "off"
}

Commands (VS Code)

  • ucode: Select target OpenWrt/ucode version
  • ucode: Show Function Git History
  • ucode: Show Function References

Architecture

  • 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 (like tsc)

Contributing

Development workflow

  1. Make changes to source files in src/.
  2. Build with bun run compile.
  3. Verify types with tsc --noEmit and run the test suite.
  4. Test interactively in the VS Code extension host.
  5. Submit a pull request.

Testing

# 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.js

License

MIT License — see LICENSE file for details.

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

0