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:
cargo run -- regenerate-stubsRegenerate 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/andAnnotations/FrameXML/: Pre-written LuaLS-style annotation.luafiles 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/@returnannotations 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.wowfor retail). Returns JSON with aversionfield (e.g.12.0.5.67602).db2/GlobalStrings/csv?build={build}&locale=enUS: Full CSV export of theGlobalStringsDB2 table for the given build. Columns:ID,BaseTag(variable name),TagText_lang(English string value),Flags. Used to generateGlobalStrings.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 theGlobalColorDB2 table (no build filter - uses latest available data for maximum coverage). Columns:ID,LuaConstantName,Color(signed int32, packed ARGB). Used to generateGlobalColors.lua. For each entry, two globals are emitted: the color object itself (typedcolorRGBA) and a_CODEstring variant (e.g.GREEN_FONT_COLORandGREEN_FONT_COLOR_CODE). The_CODEvariants are color escape strings generated at runtime by WoW's FrameXML viaGenerateHexColorMarkup().
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.luaandResources/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 forGlobalVariables.luaand per-API flavor bitmasks (derived from which branches'GlobalAPI.luacontains each name; a function retail defines in FrameXML gets no bitmask, even where Forever implements it in C). Sincelivecan trail the retail client by a patch, a function or widget method that retail'sAPIDocumentationGenerateddocuments counts as retail even beforelivelists it.Resources/LuaEnum.lua: Parsed forLE_*legacy enum constant values (nestedEnum = { Category = { ValueName = N } }structures converted from CamelCase toLE_UPPER_SNAKEformat, cross-referenced against wow-ui-source FrameXMLLE_*references);Enum.*enum category stubs (merged with APIDocumentation enumerations); and theConstantsglobal table (sub-tables with typed number/boolean/string fields).Resources/CVars.lua: Parsed for["cvarName"] = { ... }entries to generate theCVarstring 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 withname=,mixin=, andinherits=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, namedTemplate_Childas in FramexmlAnnotations.Interface/AddOns/Blizzard_APIDocumentationGenerated/*.lua: Parsed directly for structured function/event/structure data. Each file is a Lua table withType = "System"(game APIs) orType = "ScriptObject"(widget methods). Functions include namespace, arguments, returns, andMayReturnNothing. Events includeLiteralNameandPayload. Structures include typedFields. Params with aMixinfield 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@aliasdefinitions in Ketho'sBlizzardType.lua. Array params (Type = "table", InnerType = "Foo") produceFoo[]. 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-fieldNeverSecret/ConditionalSecret/SecretValue, widget aspects) becomesecret<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 noC_Secretsguard or restriction query to the secrecy it tests, so@secret-clears,@secret-restriction-guardand@secret-satisfieson those come from a curated table insecret_stubs.rsthat 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 untaintedfrom their own annotated types in a separate pass over Ketho'sAnnotations/Core/Lua. See Secret Values.Interface/AddOns/**/*.lua: All FrameXML Lua files scanned for: top-levelUPPER_SNAKEconstant assignments (classic vs retail diff),LE_*name references (cross-referenced with BlizzardInterfaceResourcesLuaEnum.luafor values), and field/method assignments on frame globals (FrameName.field = rhs,function FrameName:method(...)) to infer field types. Also detectsPanelTemplates_SetNumTabscalls to injectnumTabs/selectedTabfields.Interface/AddOns/Blizzard_SharedXMLGame/Tooltip/TooltipDataHandler.lua(retail): its accessor table definesGameTooltip:SetInboxItem,SetHyperlinkand the other tooltip data accessors in Lua, each forwarding its arguments to theC_TooltipInfogetter of the same name (SetInboxItem→GetInboxItem). Ketho'sGameTooltipstub 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) \ retailare parsed to extract parameter types, names, and return types, generating typed stubs inClassicGlobals.lua. - Wiki-documented globals: Function names from the wiki category query are parsed with
parse_wikitext()to generateWikiGlobals.lua. Functions without a wiki page or whose markup can't be parsed get a barefunction name(...) endstub 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'sStructure <Name>pages, which API pages transclude. Those pages are fetched in a secondSpecial:Exportrequest (cached the same way) and parsed withparse_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):
| File | Purpose |
|---|---|
AceAddon-3.0.lua | AceAddon library stubs; the library class inherits the embeddable AceAddon prototype so ---@class Foo : AceAddon-3.0 resolves NewModule/GetModule/… |
AceDB-3.0.lua | AceDB 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.lua | AceEvent 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.lua | AceGUI library stubs |
AceLocale-3.0.lua | AceLocale 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.lua | AceTimer-3.0 ScheduleTimer/ScheduleRepeatingTimer with a method-name callback typed keyof self (the rest of AceTimer stays vendored) |
BattlePetTooltip.lua | Runtime-injected AddLine method that Ketho's vendor annotation omits |
CallbackRegistryMixin.lua | @generates-events on GenerateCallbackEvents so the synthesized .Event enum table resolves |
ClassicLegacyEnums.lua | LE_* constants used by addons but not referenced by FrameXML (so not auto-discovered) |
coroutine.lua | Coroutine yield/resume types |
CreateFont.lua | @creates-global for CreateFont/CreateFontFamily named-font side effect |
CreateFrame.lua | Intersection types (CreateFrame(..., template) → T & Tp) and @creates-global for the named-frame side effect |
debugstack.lua | Debug stack trace function |
EnumUtil.lua | @returns-enum on EnumUtil.MakeEnum, so EnumUtil.MakeEnum("A", "B") is typed as { A = 1, B = 2 } |
EquipmentManager.lua | Full-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.lua | Cursor info return type overloads |
GetObjectType.lua | @returns-class-name on FrameScriptObject:GetObjectType for equality-comparison receiver narrowing (region:GetObjectType() == "FontString") |
HookScript.lua | Event handler hook typing |
hooksecurefunc.lua | Backtick-generic @overload for the table-less hooksecurefunc(name, hook) form |
ipairs.lua | Generic iterator with K!, V! (non-nil keys/values) |
IsObjectType.lua | @type-narrows for IsObjectType() → frame subclass narrowing |
LibDBIcon-1.0.lua | LibDBIcon stubs with the db options table fields optional (no false missing-fields on Register) |
LibSharedMedia-3.0.lua | Full replacement of the vendored LibSharedMedia annotations: mediatype is string (not a closed enum) so custom media types registered via :Register type-check |
LibStub.lua | Library version management |
loadstring.lua | loadstring return tuple (compiled chunk, or nil + error message) |
MixinFunctions.lua | Variadic generics for Mixin/CreateFromMixins/CreateAndInitFromMixin: (T, ...M) → T & ...M |
NamePlateBaseMixin.lua | Base mixin for name plates |
newproxy.lua | Userdata proxy creation |
next.lua | Generic next iterator with K!, V! |
osdate.lua | date("*t") fields typed integer rather than the vendor's integer-or-string |
pairs.lua | Generic iterator with K!, V! (non-nil keys/values) |
pcall.lua | Generic success/error tuple returns |
pcallwithenv.lua | Generic pcall variant with environment |
PlaySound.lua | String channel override for uiSoundSubType (Blizzard docs say enum, Lua takes strings) |
plugin_api.lua | Plugin diagnostic API types |
Pools.lua | Generic ObjectPool<T>/FramePool/FramePoolCollection types (FrameXML-defined, no upstream source) |
RuntimeMissingGlobals.lua | Globals 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.lua | returns<F> projection for variadic return truncation |
SetOwner.lua | GameTooltip:SetOwner with an untyped owner, so any region (including an addon mixin passing self) can own the tooltip |
SetScript.lua | Contextual callback typing with event-param narrowing |
string_match.lua | Pattern matching return types |
table.lua | Generic overloads for insert, remove, etc., and @stores-into on insert so inserting into {} types the list's elements |
tonumber.lua | tonumber base-argument overload + @nodiscard |
unpack.lua | Variadic unpacking with ...T syntax |
WorldFrame.lua | Global 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:
| File | Source |
|---|---|
GlobalStrings.lua | wago.tools db2/GlobalStrings (latest retail build, enUS locale) |
GlobalColors.lua | wago.tools db2/GlobalColor (~360 colorRGBA objects + _CODE string variants) |
GlobalVariables.lua | BlizzardInterfaceResources global name lists (names not covered by GlobalStrings or GlobalColors) |
ClassicGlobals.lua | BlizzardInterfaceResources diff + wiki scraping + APIDocumentation + LE_* constants + XML frame globals |
WikiGlobals.lua | Wiki category query function names + wiki scraping |
BlizzardAPI.lua | Blizzard APIDocumentationGenerated functions (deduped against Ketho) |
BlizzardStructures.lua | Blizzard APIDocumentationGenerated structure types (deduped against Ketho) |
BlizzardEvents.lua | Blizzard APIDocumentationGenerated events |
BlizzardEnums.lua | APIDocumentation enumerations merged with LuaEnum.lua categories |
ScriptObjectMethods.lua | Blizzard APIDocumentationGenerated widget methods missing from Ketho's stubs |
CVars.lua | BlizzardInterfaceResources CVars.lua string literal union |
Constants.lua | LuaEnum.lua Constants table (57 sub-tables with typed fields) |
FrameXMLUtilities.lua | wow-ui-source utility tables and mixins not covered by other stubs |
FrameXMLTemplates.lua | wow-ui-source XML virtual templates not covered by other stubs, with their children |
InferredReturns.lua | Return types inferred by analyzing wow-ui-source for FrameXML functions that have no @return |
InferredFields.lua | Undeclared runtime fields read on stub classes in wow-ui-source, typed any? |
Output artifacts
| File | Format | Content |
|---|---|---|
stubs/precomputed.bin.zst | Magic (0x574F575F) + Version (4B) + zstd-9 payload | Bincode-serialized PrecomputedStubs (PreResolvedGlobals + ClassDecl + ExternalGlobal) |
stubs/precomputed-files.bin.zst | Version (4B) + zstd-9 payload | Bincode-serialized HashMap<String, String> of stub file contents for go-to-definition |
stubs/precomputed-provenance.txt | Text | Generation 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:
# 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