Skip to content

Stub Generation ​

wowlua-ls ships with precomputed type stubs for the entire WoW API: functions, classes, enums, events, and global variables across retail, classic, classic era, and WoW Forever. This page documents the data sources, pipeline, and output artifacts that make this work.

Stubs are regenerated with:

bash
cargo run -- regenerate-stubs

Regenerate after any change to crates/wowlua_stub_gen/, crates/wowlua_analysis/src/xml_scan.rs (the pipeline reuses it for FrameXML frames), or stubs/overrides/, and commit both regenerated blobs (stubs/precomputed.bin.zst and stubs/precomputed-files.bin.zst) along with the updated stubs/precomputed-provenance.txt.

The pipeline lives in crates/wowlua_stub_gen/src/stub_gen/, split by stage: orchestrate.rs (the regenerate_stubs() entry point, which runs every stage and writes the blobs), sources.rs (fetching, cloning, and caches), one module per data source below (blizzard.rs, wiki.rs, globals_csv.rs, framexml.rs, xml_frames.rs, classic.rs), the stub rewrites (secret_stubs.rs and tooltip_accessors.rs edit stub text in place; data_params.rs retypes mixin-object parameters such as colorRGB and ItemLocation to a Data | Object union after scanning), and util.rs (path collection and count validation).

The git repositories below (Ketho/vscode-wow-api and Gethe/wow-ui-source) are shallow-cloned into a persistent cache under the platform cache directory ($XDG_CACHE_HOME/~/.cache/wowlua-ls/clones/ on Linux/macOS, %LOCALAPPDATA%\wowlua-ls\clones\ on Windows). On subsequent runs the cached clones are updated in place with git fetch --depth 1 + git reset --hard rather than re-cloned from scratch, saving most of the clone time. Set the WOWLUA_LS_REFRESH_CLONES environment variable to force fresh clones.

Data sources ​

The pipeline integrates six major sources into a single type database.

1. Ketho/vscode-wow-api ​

Ketho/vscode-wow-api is shallow-cloned with its submodules (which pulls in NumyAddon/FramexmlAnnotations into Annotations/FrameXML/).

  • Annotations/Core/ and Annotations/FrameXML/: Pre-written LuaLS-style annotation .lua files containing type stubs for classes, functions, and fields related to frames and widgets. These are scanned directly as Lua. Widget stubs with a ---[Documentation](...) link but no @param/@return annotations are enriched by fetching the linked wiki page and injecting parsed type annotations into the vendor files before scanning.

The submodule initialization fetches NumyAddon/FramexmlAnnotations into Annotations/FrameXML/. Ketho/BlizzardInterfaceResources data is accessed via GitHub raw URLs rather than the local clone.

2. wago.tools ​

wago.tools provides database exports via HTTP API.

  • api/builds/{product}/latest: Fetched to discover the latest build number for a given WoW product (e.g. wow for retail). Returns JSON with a version field (e.g. 12.0.5.67602).
  • db2/GlobalStrings/csv?build={build}&locale=enUS: Full CSV export of the GlobalStrings DB2 table for the given build. Columns: ID, BaseTag (variable name), TagText_lang (English string value), Flags. Used to generate GlobalStrings.lua. CRLF line endings in values are normalised to LF. All valid identifier names not already covered by hand-written stubs are emitted directly.
  • db2/GlobalColor/csv: Full CSV export of the GlobalColor DB2 table (no build filter - uses latest available data for maximum coverage). Columns: ID, LuaConstantName, Color (signed int32, packed ARGB). Used to generate GlobalColors.lua. For each entry, two globals are emitted: the color object itself (typed colorRGBA) and a _CODE string variant (e.g. GREEN_FONT_COLOR and GREEN_FONT_COLOR_CODE). The _CODE variants are color escape strings generated at runtime by WoW's FrameXML via GenerateHexColorMarkup().

3. Ketho/BlizzardInterfaceResources ​

Ketho/BlizzardInterfaceResources is fetched via raw GitHub URLs across four branches: live, classic_era, classic, forever. Each branch is resolved to its current commit when the run starts, every file is fetched at that commit, and the commits are recorded in stubs/precomputed-provenance.txt, so a regeneration that changes an API's flavors can be traced to the upstream change.

  • Resources/GlobalAPI.lua and Resources/FrameXML.lua: Simple name lists used to compute the classic-only API diff by identifying names present in the other branches but absent from retail/live. Also used as the source of global variable names for GlobalVariables.lua and per-API flavor bitmasks (derived from which branches' GlobalAPI.lua contains each name; a function retail defines in FrameXML gets no bitmask, even where Forever implements it in C). Since live can trail the retail client by a patch, a function or widget method that retail's APIDocumentationGenerated documents counts as retail even before live lists it.
  • Resources/LuaEnum.lua: Parsed for LE_* legacy enum constant values (nested Enum = { Category = { ValueName = N } } structures converted from CamelCase to LE_UPPER_SNAKE format, cross-referenced against wow-ui-source FrameXML LE_* references); Enum.* enum category stubs (merged with APIDocumentation enumerations); and the Constants global table (sub-tables with typed number/boolean/string fields).
  • Resources/CVars.lua: Parsed for ["cvarName"] = { ... } entries to generate the CVar string literal union type alias.

4. Gethe/wow-ui-source ​

Gethe/wow-ui-source is shallow-cloned across four branches: live, classic_era, classic, forever.

  • Interface/AddOns/**/*.xml: XML files parsed via regex to extract <Frame>, <Button> elements with name=, mixin=, and inherits= attributes, producing frame global names with type and mixin associations. Inheritance is resolved transitively with cycle detection. Virtual templates without a class in any other stub get one from the scanner that reads workspace XML, on all four branches. A template defined differently on several branches, or in several flavors' directories, gets every definition's parents and children, retail's types taking precedence. A child that combines its widget type with templates, mixins, or children of its own gets a class too, named Template_Child as in FramexmlAnnotations.
  • Interface/AddOns/Blizzard_APIDocumentationGenerated/*.lua: Parsed directly for structured function/event/structure data. Each file is a Lua table with Type = "System" (game APIs) or Type = "ScriptObject" (widget methods). Functions include namespace, arguments, returns, and MayReturnNothing. Events include LiteralName and Payload. Structures include typed Fields. Params with a Mixin field use the mixin name (Lua class) instead of the C++ Type. Types are normalized minimally: bool→boolean, cstring→string, luaIndex→number; all other type names (e.g. WOWGUID, fileID, time_t) are kept as-is since they have @alias definitions in Ketho's BlizzardType.lua. Array params (Type = "table", InnerType = "Foo") produce Foo[]. Generated stubs only fill gaps: functions/structures already covered by Ketho's richer annotations are skipped via name deduplication. Also parsed for classic-only constants (structured {Name, Type, Value} entries) and enumerations ({Name, EnumValue} entries). On the retail branch, secret-value keys (SecretReturns, SecretWhen… predicates, SecretArguments, ConstSecretAccessor, the secret predicate table's preconditions, per-field NeverSecret/ConditionalSecret/SecretValue, widget aspects) become secret<T> types and @secret-* annotations on the matching functions, widget methods, events, and structure fields (a structure's class also records the predicates of the APIs that return it), both in the generated files and in Ketho's vendor stubs (rewritten in place before scanning). The docs link no C_Secrets guard or restriction query to the secrecy it tests, so @secret-clears, @secret-restriction-guard and @secret-satisfies on those come from a curated table in secret_stubs.rs that quotes each guard's documentation, naming the parameters the covered APIs bind each guard argument to. The Lua library isn't documented there at all: its number-typed parameters get @secret-args untainted from their own annotated types in a separate pass over Ketho's Annotations/Core/Lua. See Secret Values.
  • Interface/AddOns/**/*.lua: All FrameXML Lua files scanned for: top-level UPPER_SNAKE constant assignments (classic vs retail diff), LE_* name references (cross-referenced with BlizzardInterfaceResources LuaEnum.lua for values), and field/method assignments on frame globals (FrameName.field = rhs, function FrameName:method(...)) to infer field types. Also detects PanelTemplates_SetNumTabs calls to inject numTabs/selectedTab fields.
  • Interface/AddOns/Blizzard_SharedXMLGame/Tooltip/TooltipDataHandler.lua (retail): its accessor table defines GameTooltip:SetInboxItem, SetHyperlink and the other tooltip data accessors in Lua, each forwarding its arguments to the C_TooltipInfo getter of the same name (SetInboxItem → GetInboxItem). Ketho's GameTooltip stub is widened in place to accept each getter's documented arguments, and accessors it lacks are added.

5. warcraft.wiki.gg ​

warcraft.wiki.gg provides community-maintained API documentation.

Function names are discovered by querying the MediaWiki API for the API_functions, API_functions/Removed, API_functions/deprecated, and API_functions/Noflavor categories. Names that duplicate a Blizzard API namespace function (e.g. GetAddOnMetadata shadowed by C_AddOns.GetAddOnMetadata) are filtered unless they still exist as bare globals in BlizzardInterfaceResources' GlobalAPI.lua.

Page names from all three passes are collected upfront and batch-fetched from Special:Export in sequential POSTs of 4,000 titles (a single request for the whole set runs past the wiki's time limit and comes back cut short; the endpoint is behind Cloudflare, which rejects concurrent requests, so the requests never run in parallel), then distributed to each processor:

The raw Special:Export XML dump is the single most expensive step (about 75 s). It is persistently cached under the platform cache directory ($XDG_CACHE_HOME/~/.cache/wowlua-ls/ on Linux/macOS, %LOCALAPPDATA%\wowlua-ls\ on Windows) with a 24-hour TTL. Only a complete dump, one that ends with the closing </mediawiki> tag, is cached or reused; a dump the wiki cuts short is fetched again. The cache is keyed by a hash of the requested page-name set (not by parsing logic) so iterating on parse_wikitext() or stub formatting reuses the cached dump and only a change to which pages are requested forces a re-fetch. Set the WOWLUA_LS_REFRESH_WIKI environment variable to force a fresh fetch regardless of cache age.

  • Classic-only APIs: Wiki pages for APIs in (classic_era ∪ classic ∪ forever) \ retail are parsed to extract parameter types, names, and return types, generating typed stubs in ClassicGlobals.lua.
  • Wiki-documented globals: Function names from the wiki category query are parsed with parse_wikitext() to generate WikiGlobals.lua. Functions without a wiki page or whose markup can't be parsed get a bare function name(...) end stub with a doc link.
  • Widget method enrichment: Vendor widget stubs that have a doc link but no annotations are enriched by parsing type annotations via parse_widget_wiki_annotations().
  • Secret-value structure fields: Structures that Blizzard's API documentation references but doesn't define (such as AuraData) are described on the wiki's Structure <Name> pages, which API pages transclude. Those pages are fetched in a second Special:Export request (cached the same way) and parsed with parse_wiki_structure(); their {{apitype|type|secret=NeverSecret}} marks feed the same secret-value marking as Blizzard's own structures. A page that marks no field's secrecy isn't used.

Wiki parsing handles {{apisig|...}} templates, == Arguments == / == Returns == sections, {{apitype|type|nilable}} type annotations, embedded <!-- luals ... --> blocks, optional parameters [, param], and redirect resolution (a name's own page wins over a legacy title that redirects elsewhere).

6. Local overrides ​

Hand-written override files in stubs/overrides/ take precedence over vendor stubs: an override's function and method definitions replace the vendor's, whatever the file is named. Where the vendor redeclares an overridden method on subclasses with the same parameters, as Ketho does for SetScript and HookScript on every widget type to narrow the script-name type, those redeclarations keep their own signature and gain the override's @overloads. An override whose filename stem matches a vendor file (such as LibStub.lua) also replaces that whole vendor file, along with the pipeline's in-place rewrites of it (wiki enrichment, secret-value annotations, tooltip data accessors), so an override that changes individual methods gets a stem of its own (such as SetOwner.lua). Overrides handle cases that require wowlua-ls-specific annotations not expressible in standard LuaLS (generics, intersections, variadic types, etc.). The full set (47 files, alphabetical - keep in sync with ls stubs/overrides/*.lua):

FilePurpose
AceAddon-3.0.luaAceAddon library stubs; the library class inherits the embeddable AceAddon prototype so ---@class Foo : AceAddon-3.0 resolves NewModule/GetModule/…
AceDB-3.0.luaAceDB library stubs; AceDB:New/db:RegisterNamespace are generic over the defaults table, returning Defaults & AceDBObject-3.0 so the declared default sections/fields are typed on the DB object while its methods stay available. Sections are non-nil table so indexing them doesn't trip need-check-nil. db.RegisterCallback types a string method (or, when it is omitted, the event name) as a key of the target
AceEvent-3.0.luaAceEvent library stubs; RegisterEvent/RegisterMessage type the handler as keyof self so the handler string navigates to (and is checked against) the method on self
AceGUI-3.0.luaAceGUI library stubs
AceLocale-3.0.luaAceLocale library stubs; default-locale NewLocale(app, locale, true) returns a non-nil table so the L[key] = true idiom doesn't trip need-check-nil
AceTimer.luaAceTimer-3.0 ScheduleTimer/ScheduleRepeatingTimer with a method-name callback typed keyof self (the rest of AceTimer stays vendored)
BattlePetTooltip.luaRuntime-injected AddLine method that Ketho's vendor annotation omits
CallbackRegistryMixin.lua@generates-events on GenerateCallbackEvents so the synthesized .Event enum table resolves
ClassicLegacyEnums.luaLE_* constants used by addons but not referenced by FrameXML (so not auto-discovered)
coroutine.luaCoroutine yield/resume types
CreateFont.lua@creates-global for CreateFont/CreateFontFamily named-font side effect
CreateFrame.luaIntersection types (CreateFrame(..., template) → T & Tp) and @creates-global for the named-frame side effect
debugstack.luaDebug stack trace function
EnumUtil.lua@returns-enum on EnumUtil.MakeEnum, so EnumUtil.MakeEnum("A", "B") is typed as { A = 1, B = 2 }
EquipmentManager.luaFull-arity @return for EquipmentManager_UnpackLocation (deprecated on retail with no @return, still live on Classic) so destructuring its result doesn't false-positive unbalanced-assignments
EventRegistry.lua@class EventRegistry : CallbackRegistryMixin with FrameEvent-typed callback params
GetCursorInfo.luaCursor info return type overloads
GetObjectType.lua@returns-class-name on FrameScriptObject:GetObjectType for equality-comparison receiver narrowing (region:GetObjectType() == "FontString")
HookScript.luaEvent handler hook typing
hooksecurefunc.luaBacktick-generic @overload for the table-less hooksecurefunc(name, hook) form
ipairs.luaGeneric iterator with K!, V! (non-nil keys/values)
IsObjectType.lua@type-narrows for IsObjectType() → frame subclass narrowing
LibDBIcon-1.0.luaLibDBIcon stubs with the db options table fields optional (no false missing-fields on Register)
LibSharedMedia-3.0.luaFull replacement of the vendored LibSharedMedia annotations: mediatype is string (not a closed enum) so custom media types registered via :Register type-check
LibStub.luaLibrary version management
loadstring.lualoadstring return tuple (compiled chunk, or nil + error message)
MixinFunctions.luaVariadic generics for Mixin/CreateFromMixins/CreateAndInitFromMixin: (T, ...M) → T & ...M
NamePlateBaseMixin.luaBase mixin for name plates
newproxy.luaUserdata proxy creation
next.luaGeneric next iterator with K!, V!
osdate.luadate("*t") fields typed integer rather than the vendor's integer-or-string
pairs.luaGeneric iterator with K!, V! (non-nil keys/values)
pcall.luaGeneric success/error tuple returns
pcallwithenv.luaGeneric pcall variant with environment
PlaySound.luaString channel override for uiSoundSubType (Blizzard docs say enum, Lua takes strings)
plugin_api.luaPlugin diagnostic API types
Pools.luaGeneric ObjectPool<T>/FramePool/FramePoolCollection types (FrameXML-defined, no upstream source)
RuntimeMissingGlobals.luaGlobals used by addons but not in BlizzardInterfaceResources
SecretValues.lua@secret-guard on issecretvalue/canaccessvalue/canaccessallvalues/hasanysecretvalues, secret<T> returns for secretwrap and unwrapped returns for secretunwrap, and @secret-args tainted on the string functions Blizzard documents as accepting secrets
select.luareturns<F> projection for variadic return truncation
SetOwner.luaGameTooltip:SetOwner with an untyped owner, so any region (including an addon mixin passing self) can own the tooltip
SetScript.luaContextual callback typing with event-param narrowing
string_match.luaPattern matching return types
table.luaGeneric overloads for insert, remove, etc., and @stores-into on insert so inserting into {} types the list's elements
tonumber.luatonumber base-argument overload + @nodiscard
unpack.luaVariadic unpacking with ...T syntax
WorldFrame.luaGlobal frame instance

Generated stub files ​

The pipeline writes these Lua files to a temp directory for scanning. Their text ships in the files blob under generated/, so go-to-definition can open them:

FileSource
GlobalStrings.luawago.tools db2/GlobalStrings (latest retail build, enUS locale)
GlobalColors.luawago.tools db2/GlobalColor (~360 colorRGBA objects + _CODE string variants)
GlobalVariables.luaBlizzardInterfaceResources global name lists (names not covered by GlobalStrings or GlobalColors)
ClassicGlobals.luaBlizzardInterfaceResources diff + wiki scraping + APIDocumentation + LE_* constants + XML frame globals
WikiGlobals.luaWiki category query function names + wiki scraping
BlizzardAPI.luaBlizzard APIDocumentationGenerated functions (deduped against Ketho)
BlizzardStructures.luaBlizzard APIDocumentationGenerated structure types (deduped against Ketho)
BlizzardEvents.luaBlizzard APIDocumentationGenerated events
BlizzardEnums.luaAPIDocumentation enumerations merged with LuaEnum.lua categories
ScriptObjectMethods.luaBlizzard APIDocumentationGenerated widget methods missing from Ketho's stubs
CVars.luaBlizzardInterfaceResources CVars.lua string literal union
Constants.luaLuaEnum.lua Constants table (57 sub-tables with typed fields)
FrameXMLUtilities.luawow-ui-source utility tables and mixins not covered by other stubs
FrameXMLTemplates.luawow-ui-source XML virtual templates not covered by other stubs, with their children
InferredReturns.luaReturn types inferred by analyzing wow-ui-source for FrameXML functions that have no @return
InferredFields.luaUndeclared runtime fields read on stub classes in wow-ui-source, typed any?

Output artifacts ​

FileFormatContent
stubs/precomputed.bin.zstMagic (0x574F575F) + Version (4B) + zstd-9 payloadBincode-serialized PrecomputedStubs (PreResolvedGlobals + ClassDecl + ExternalGlobal)
stubs/precomputed-files.bin.zstVersion (4B) + zstd-9 payloadBincode-serialized HashMap<String, String> of stub file contents for go-to-definition
stubs/precomputed-provenance.txtTextGeneration timestamp, the commit of vscode-wow-api and of each wow-ui-source and BlizzardInterfaceResources branch, symbol/function/table/file counts

The main blob contains the fully resolved type database, and the files blob contains the source text of every referenced stub file so the LSP can support go-to-definition into stub code. Every source path in the main blob is a key of the files blob (vendor/… for the vscode-wow-api clone, overrides/…, generated/…), so neither blob contains paths from the machine that generated it.

The blob version (BLOB_VERSION in crates/wowlua_analysis/src/pre_globals/mod.rs) is incremented whenever PreResolvedGlobals, ClassDecl, ExternalGlobal, or any serialized type changes shape.

Loading ​

The embedded-stubs Cargo feature (default on) bakes both blobs into the binary via include_bytes!. Without the feature (--no-default-features), blobs are loaded at runtime from a stubs/ directory next to the executable, used for universal editor plugin packages that share one copy of stubs across per-platform binaries.

Validation ​

Every download gives up on a server that stops responding: an HTTP request after two minutes without a response or five minutes on its body, a git transfer after two minutes without progress. HTTP downloads are retried after a transient failure (a timeout, a dropped connection, HTTP 408, 429 or 5xx, or a response cut short). A source that still fails, including a single page of the wiki's category listing, aborts the run before it writes the stubs.

Before writing, the pipeline validates minimum counts (symbols ≥ 50k, functions ≥ 20k, tables ≥ 10k, files ≥ 1k, globals ≥ 50k, classes ≥ 10k, FrameXML templates ≥ 2k) to catch truncated data from network failures or upstream structure changes. It also refuses to write the blobs if either one contains a directory the run used (the clone cache, its temp directory, or stubs/overrides).

The dump-stubs CLI subcommand outputs every global name and its resolved type as a tab-separated list, sorted alphabetically. This is useful for diffing before and after stub regeneration to catch regressions:

bash
# Save baseline before changes
wowlua_ls dump-stubs > before.txt

# After regenerating stubs, diff
wowlua_ls dump-stubs > after.txt
diff before.txt after.txt