wow-secret-lint 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0 - 2026-09-07
4
+
5
+ Patch 12.1.5 (build 69594) went live on 2026-09-03. This catches the linter up to it: the
6
+ snapshot, three new rules, and a new default `--patch` surface. `--patch=12.0` and
7
+ `--patch=12.1` produce v1.2.0's and v1.4.2's output byte for byte, which is now enforced by
8
+ recorded baselines rather than asserted.
9
+
10
+ ### Added
11
+
12
+ - **WSL019** (error): querying the progress or running state of an animation that carries
13
+ the new `QueryAnimationProgress` forbidden aspect, which *"prevents querying the progress
14
+ of an animation or whether it is running"*.
15
+ - **WSL020** (error): adding an animation to, or reparenting one onto, a group that carries
16
+ the new `AddAnimations` forbidden aspect, which *"prevents animations from being added or
17
+ reparented"*. The reparenting half is `SimpleAnim:SetParent`, where the aspect is checked
18
+ on the `parent` argument rather than on the receiver.
19
+ - **WSL021** (error): *"the `SetCooldown` and `Clear` cooldown APIs can no longer be called
20
+ from tainted code when the cooldown frame itself is protected."* Blizzard marked six
21
+ `FrameAPICooldown` methods `IsProtectedFunction = true` in 12.1.5 and none in 12.1, so the
22
+ method list is read from the snapshot rather than transcribed.
23
+ - **`--patch=12.1.5`, and it is the new default.** `PATCHES` is ordered now, so rules gate on
24
+ "at or after" a surface instead of a not-equals check against `12.0`.
25
+ - **`--patch=auto`** reads the addon's own `## Interface` number and checks the surface it
26
+ actually ships against: 120105 and up gets 12.1.5, 120100 gets 12.1, older gets 12.0. It
27
+ says so and falls back to the default when there is no `.toc` to read.
28
+ - **`--refresh-ref=<ref>`** rebuilds the snapshot from any branch or tag of the mirror, and
29
+ `--force` overrides the new guard that stops a refresh walking the snapshot backwards onto
30
+ an older client build. The mirror tags a build before it moves `live`, which is how 12.1.5
31
+ arrived here on the day it shipped; the weekly refresh workflow now reports that as a
32
+ no-op instead of a downgrade.
33
+
34
+ ### Changed
35
+
36
+ - Refreshed `data/api-snapshot.json` from `Gethe/wow-ui-source@12.1.5`: 10,250 documented
37
+ functions and 760 structures, up from 10,099 and 752, with 20 `SecretReturns` (unchanged)
38
+ and 314 conditionally secret (up from 310). Every count is regenerated, not carried over.
39
+ The new globals are in it, so WSL006 and WSL007 stop analysing 12.1.5 code against a
40
+ snapshot that has never heard of `math.clamp`, `string.startswith`, `table.keys`,
41
+ `C_Intl`, `C_Weather`, `C_Timer.NewTimedSignalMap` or `CreateFrameWithOptions`. A file
42
+ passing a secret to `math.clamp`, `C_Intl.ToUpper` and `string.startswith` reports three
43
+ WSL006 errors against this snapshot and zero against 1.4.2's, which is the whole point of
44
+ the refresh. It also brought in a new conditional marker with no code change:
45
+ `SecretWhenLuaTableHasSecretKeys` on `table.count`, `table.getcountinfo` and
46
+ `table.isempty`.
47
+ - The snapshot now carries two more Blizzard markers, keyed by widget system because method
48
+ names collide across widget types: `ChecksForbiddenAspects` (which forbidden aspect a
49
+ method checks, and on which argument) and `IsProtectedFunction`. It also records the patch,
50
+ build, commit and ref it was generated from.
51
+ - A `local` bound to a folded string constant (`"PetActionButton" .. 4 .. "Cooldown"`) now
52
+ resolves the same way a plain string literal always did, and a numeric for-loop counter
53
+ inside one folds to a digit, so `_G["ActionButton" .. i .. "Cooldown"]` is recognised in
54
+ the loop every action-bar addon writes. A local or parameter that shares a Blizzard frame's
55
+ name is never taken for the frame.
56
+ - The GitHub Action's `patch` input defaults to `12.1.5` and accepts `auto`.
57
+
58
+ ### Verified
59
+
60
+ - 230 tests pass. `--patch=12.0` and `--patch=12.1`, with and without
61
+ `--strict --conditional=warn`, reproduce v1.4.2's output over the whole fixture corpus
62
+ byte for byte, recorded before any of this work started and checked by
63
+ `test/patch1215.test.mjs`.
64
+ - On the 12-addon corpus the README measures, `--patch=12.1.5` reports the same 379 errors
65
+ and 121 warnings as `--patch=12.1`: the three new rules add nothing and take nothing away.
66
+ A second pass over each addon's current HEAD plus OmniCC, Dominos and Blizzard's own
67
+ interface code, 4,076 further files, also reports nothing from them. Four days in, nobody
68
+ has adopted the Pandemic animation APIs yet.
69
+ - One candidate finding was found and thrown out. An early draft treated a `Cooldown` created
70
+ under a secure-template button as protected, which flagged SpartanUI's quest button.
71
+ `ScriptRegion:IsProtected` says protection runs the other way: *"anchoring or parenting a
72
+ protected frame to another frame makes that frame implicitly protected as well"*, so the
73
+ child is not the one that changes. The inference was dropped and the rule now fires only
74
+ where the file names one of Blizzard's action-button cooldowns or builds a `Cooldown` from
75
+ a secure template.
76
+
3
77
  ## 1.4.2 - 2026-08-31
4
78
 
5
79
  Data only. No rule, message or analysis behaviour changed.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Static analysis for World of Warcraft **retail** addons. It maps where your Lua touches Blizzard's secret-value API, and hard-fails your build on the things Blizzard documents as certain to break.
4
4
 
5
- Patch 12.0 introduced [secret values](https://warcraft.wiki.gg/wiki/Secret_Values). A lot of the API now hands your addon a value you are allowed to store, pass around and print, but not to do arithmetic on, compare, index, call, or measure with `#`. Patch 12.1 (Curse of Ula'tek, live 2026-08-11) [widened that surface enormously](https://warcraft.wiki.gg/wiki/Patch_12.1.0/API_changes): every `UnitAura` API returns full secrets or nil while auras are secret, seventeen unit identity APIs joined them, and the new AuraButton/AuraContainer frames carry forbidden aspects. When tainted code does something disallowed, the wiki is blunt about the result:
5
+ Patch 12.0 introduced [secret values](https://warcraft.wiki.gg/wiki/Secret_Values). A lot of the API now hands your addon a value you are allowed to store, pass around and print, but not to do arithmetic on, compare, index, call, or measure with `#`. Patch 12.1 (Curse of Ula'tek, live 2026-08-11) [widened that surface enormously](https://warcraft.wiki.gg/wiki/Patch_12.1.0/API_changes): every `UnitAura` API returns full secrets or nil while auras are secret, seventeen unit identity APIs joined them, and the new AuraButton/AuraContainer frames carry forbidden aspects. Patch 12.1.5 (build 69594, live 2026-09-03) [added two more forbidden aspects and closed the cooldown APIs](https://warcraft.wiki.gg/wiki/Patch_12.1.5/API_changes). When tainted code does something disallowed, the wiki is blunt about the result:
6
6
 
7
7
  > When an operation that is not allowed is performed, the result will be an **immediate** Lua error.
8
8
 
@@ -17,7 +17,7 @@ That one is [KkthnxUI#121](https://github.com/Kkthnx-Wow/KkthnxUI/issues/121), f
17
17
 
18
18
  `wow-secret-lint` reads Blizzard's own generated API documentation, tracks which of your locals hold a secret, and tells you where you touch one.
19
19
 
20
- **It reports, it does not accuse.** The rules that fail your build by default are the ones Blizzard states outright: `WSL008` (combat log registration errors), and the 12.1 rules `WSL012`/`WSL013` (aura and identity APIs "will now either return full secrets or nil when called by addons"), `WSL014` (the symbol no longer exists) and `WSL017` (forbidden aspects). The 12.0-era `SecretReturns` tier stays a warning, because whether those APIs really hand a secret to tainted code on every call is not something static analysis can confirm; run `--strict` to gate on it. [The reasoning is spelled out below](#the-open-question-on-severity) and it is worth two minutes before you wire this into CI.
20
+ **It reports, it does not accuse.** The rules that fail your build by default are the ones Blizzard states outright: `WSL008` (combat log registration errors), the 12.1 rules `WSL012`/`WSL013` (aura and identity APIs "will now either return full secrets or nil when called by addons"), `WSL014` (the symbol no longer exists) and `WSL017` (forbidden aspects), and the 12.1.5 rules `WSL019`/`WSL020` (the two new forbidden aspects) and `WSL021` (protected cooldown frames). The 12.0-era `SecretReturns` tier stays a warning, because whether those APIs really hand a secret to tainted code on every call is not something static analysis can confirm; run `--strict` to gate on it. [The reasoning is spelled out below](#the-open-question-on-severity) and it is worth two minutes before you wire this into CI.
21
21
 
22
22
  ## Install
23
23
 
@@ -93,6 +93,47 @@ Exit code 1. Under `--patch=12.0` the same file reports zero findings and exits
93
93
 
94
94
  None of this comes from Blizzard's generated documentation, which carries no marker for aura secrecy. The 12.1 behaviour is published only in the patch notes, so these rules seed their taint from a list transcribed there, and `--patch` selects whether that list is active.
95
95
 
96
+ ### The 12.1.5 surface
97
+
98
+ Patch 12.1.5 gave `CustomAuraButton` Pandemic-triggered animations, and paid for them with two new forbidden aspects: *"`QueryAnimationProgress`: prevents querying the progress of an animation or whether it is running"* and *"`AddAnimations`: prevents animations from being added or reparented."* The same notes close the cooldown APIs: *"the `SetCooldown` and `Clear` cooldown APIs can no longer be called from tainted code when the cooldown frame itself is protected."*
99
+
100
+ `Core/Pandemic.lua` builds a glow the documented way and then keeps poking it:
101
+
102
+ ```lua
103
+ local group = glow:CreateAnimationGroup()
104
+ local fade = group:CreateAnimation("Alpha")
105
+ fade:SetDuration(0.5)
106
+
107
+ button:AddPandemicActiveAnimation(group)
108
+
109
+ if group:IsPlaying() then group:Stop() end
110
+ group:CreateAnimation("Scale")
111
+ ```
112
+
113
+ `Core/Cooldowns.lua` drives one of Blizzard's action buttons:
114
+
115
+ ```lua
116
+ local cooldown = _G["ActionButton1Cooldown"]
117
+ cooldown:SetCooldown(GetTime(), 10)
118
+ ```
119
+
120
+ ```
121
+ $ npx wow-secret-lint ./PandemicGlow
122
+ Core/Cooldowns.lua:2:1 error WSL021 cooldown:SetCooldown() cannot be called from tainted code in 12.1.5 because the cooldown frame itself is protected; drive your own Cooldown frame instead, or leave the protected one to Blizzard
123
+ Core/Pandemic.lua:10:4 error WSL019 querying the progress or running state of an animation group registered with a Pandemic trigger is disallowed by its QueryAnimationProgress forbidden aspect: group:IsPlaying()
124
+ Core/Pandemic.lua:11:1 error WSL020 adding an animation to an animation group registered with a Pandemic trigger is disallowed by its AddAnimations forbidden aspect: group:CreateAnimation()
125
+ 3 errors, 0 warnings
126
+ ```
127
+
128
+ Exit code 1. The first five lines of `Core/Pandemic.lua` are Blizzard's own sample from the 12.1.5 notes and stay silent: building a group and filling it is fine, and the aspects arrive with the `AddPandemicActiveAnimation` call. `--patch=12.1` reports nothing at all in either file.
129
+
130
+ If your `.toc` is still on an older interface, `--patch=auto` reads the number and checks the surface that addon actually ships against:
131
+
132
+ ```
133
+ $ npx wow-secret-lint --patch=auto ./OldAddon # ## Interface: 120100
134
+ 0 errors, 0 warnings
135
+ ```
136
+
96
137
  ### Exit codes
97
138
 
98
139
  | Code | Meaning |
@@ -126,7 +167,7 @@ Inputs:
126
167
  | Input | Default | Description |
127
168
  | --- | --- | --- |
128
169
  | `path` | `.` | addon folder, `.toc`, or `.lua`. Several allowed, space separated |
129
- | `patch` | `12.1` | patch surface to check; `12.0` pins the pre-12.1 rule set |
170
+ | `patch` | `12.1.5` | patch surface to check; an older value pins that rule set, `auto` reads the addon `.toc` |
130
171
  | `args` | `""` | extra CLI arguments, e.g. `--conditional=warn --disable=WSL011` |
131
172
  | `format` | `github` | `github`, `stylish`, or `json` |
132
173
 
@@ -134,7 +175,7 @@ Outputs: `errors`, `warnings`.
134
175
 
135
176
  ## Rules
136
177
 
137
- Every rule maps to one sentence Blizzard publishes. `wow-secret-lint --rules` prints the table with the source for each. WSL008, WSL012, WSL013, WSL014, WSL017 and WSL018 fail a build by default; the rest are warnings.
178
+ Every rule maps to one sentence Blizzard publishes. `wow-secret-lint --rules` prints the table with the source for each. WSL008, WSL012, WSL013, WSL014, WSL017, WSL018, WSL019, WSL020 and WSL021 fail a build by default; the rest are warnings.
138
179
 
139
180
  | Rule | Severity | What it catches |
140
181
  | --- | --- | --- |
@@ -156,6 +197,9 @@ Every rule maps to one sentence Blizzard publishes. `wow-secret-lint --rules` pr
156
197
  | WSL016 | warning | 12.1: `showCountdownFrame` passed to a private-aura API |
157
198
  | WSL017 | error | 12.1: forbidden-aspect operation on an AuraButton or AuraContainer |
158
199
  | WSL018 | error | 12.1: call of an aura API reached by index, slot or instance id |
200
+ | WSL019 | error | 12.1.5: querying the progress or running state of an animation that carries `QueryAnimationProgress` |
201
+ | WSL020 | error | 12.1.5: adding or reparenting an animation on a group that carries `AddAnimations` |
202
+ | WSL021 | error | 12.1.5: `SetCooldown`/`Clear` on a cooldown frame Blizzard protects |
159
203
 
160
204
  WSL008 comes from the [12.0.0 API changes](https://warcraft.wiki.gg/wiki/Patch_12.0.0/API_changes): *"COMBAT_LOG_EVENT and COMBAT_LOG_EVENT_UNFILTERED will error when trying to register them."* Use `COMBAT_LOG_EVENT_INTERNAL_UNFILTERED` or the `C_CombatLog` namespace.
161
205
 
@@ -185,6 +229,35 @@ The rename is the minimal fix, but it is probably not the one you want. Reportin
185
229
 
186
230
  **WSL017** knows a local is an AuraContainer or AuraButton when it comes from `CreateFrame("AuraContainer", ...)`/`CreateFrame("AuraButton", ...)` or arrives as the `initializeFrame` callback parameter of `AddAuraGroup`/`AddAuraSlot`/`AddItemEnchantment`, and it follows the value into a table field, so `self.buttons[i] = button` keeps the type. On those it flags `SetScript`/`HookScript`, event registration, input calls (`EnableMouse`, `RegisterForClicks`, `Click`, ...) and focus queries (`IsMouseOver`, ...), naming the forbidden aspect each one trips. Construction itself is never flagged.
187
231
 
232
+ ### The 12.1.5 rules in detail
233
+
234
+ **WSL019** and **WSL020** are the two forbidden aspects 12.1.5 added. The notes name them outright: *"`QueryAnimationProgress`: prevents querying the progress of an animation or whether it is running"* and *"`AddAnimations`: prevents animations from being added or reparented"*. Unlike the 12.1 aura rules, this surface is in Blizzard's generated documentation, so the method lists are not transcribed by hand: every affected method carries `ChecksForbiddenAspects`, and the snapshot keeps them keyed by widget system. That is 8 methods on `SimpleAnimAPI` and 8 on `SimpleAnimGroupAPI` for `QueryAnimationProgress`, and `SimpleAnimGroupAPI:CreateAnimation` plus `SimpleAnimAPI:SetParent` for `AddAnimations`. The second of those is the "or reparented" half, and it is checked on the `parent` argument rather than on the receiver.
235
+
236
+ The aspects are applied when an animation group is registered with a Pandemic trigger, which is what the notes mean by *"two new forbidden aspects were added to support this"* and *"animations used by Pandemic triggers also gain the existing `ChangeAnimationTarget` forbidden aspect"*. Blizzard's own sample in those notes builds the group, adds its animations, and registers it last, so an addon that follows that order is silent. Everything after `AddPandemicEnterAnimation`/`AddPandemicActiveAnimation`/`AddPandemicLeaveAnimation` is not:
237
+
238
+ ```lua
239
+ local group = glow:CreateAnimationGroup()
240
+ local fade = group:CreateAnimation("Alpha") -- fine, the group is still yours
241
+ button:AddPandemicActiveAnimation(group) -- from here the client owns it
242
+
243
+ if group:IsPlaying() then end -- WSL019
244
+ group:CreateAnimation("Scale") -- WSL020
245
+ ```
246
+
247
+ Tracking follows the group through aliases, into a table field, and onto the animations that were already inside it when it was registered, and drops it the moment the local is reassigned. Guards do not silence either rule, for the same reason they do not silence WSL018: the call itself is what errors.
248
+
249
+ **WSL021** covers *"the `SetCooldown` and `Clear` cooldown APIs can no longer be called from tainted code when the cooldown frame itself is protected"*. The six methods are the ones Blizzard newly marked `IsProtectedFunction = true` on `FrameAPICooldown` in 12.1.5 and marked on none of them in 12.1: `SetCooldown`, `SetCooldownDuration`, `SetCooldownFromDurationObject`, `SetCooldownFromExpirationTime`, `SetCooldownUNIX` and `Clear`. Reading a protected cooldown is untouched, and so is a cooldown frame your addon created.
250
+
251
+ A cooldown counts as protected in two cases. It is one of Blizzard's action-button cooldowns, named directly, reached through `_G[...]` including a name built by concatenation with a numeric loop counter (`"ActionButton" .. i .. "Cooldown"` resolves), or reached as the `cooldown`/`chargeCooldown`/`lossOfControlCooldown` field of the button. A local or parameter that happens to share one of those names is left alone. The button names come from `Blizzard_ActionBar/Shared/ActionBar.lua`, which names each one `"ActionButton"..i`, `"StanceButton"..i`, `"PetActionButton"..i`, `"PossessButton"..i` or `actionBarName.."Button"..i` for the seven MultiBars, and `ActionButtonTemplate.xml` names the cooldown child `$parentCooldown` with `parentKey="cooldown"`. Or your own `Cooldown` inherits a `Secure*Template`, which is what "explicitly specified as protected at the time of creation" means for an addon.
252
+
253
+ A cooldown your addon parents to a secure button is **not** protected, and that took a false positive to get right. `ScriptRegion:IsProtected` says protection travels the other way: *"anchoring or parenting a protected frame to another frame makes that frame implicitly protected as well"*, recursively, so the child is not the one that changes.
254
+
255
+ ```lua
256
+ local cd = _G["ActionButton1Cooldown"]
257
+ cd:SetCooldown(GetTime(), 10) -- WSL021
258
+ cd:IsPaused() -- silent, reading is not a protected function
259
+ ```
260
+
188
261
  ### What it will never flag
189
262
 
190
263
  The wiki has an explicit allowed list, and flagging any of it is a false positive that gets a linter uninstalled. Each of these has a passing negative test in `test/`:
@@ -216,7 +289,7 @@ npx wow-secret-lint ./MyAddon --secret-guard=IsLocked --access-guard=CanRead
216
289
 
217
290
  Blizzard marks a second tier in the generated docs: functions that return a secret only while a specific restriction is active. `C_Spell.GetSpellCooldown` carries `SecretWhenCooldownsRestricted`; `C_LFGList.GetSearchResultInfo` carries `SecretInChatMessagingLockdown`; `UnitGUID`, `UnitName` and `UnitClass` carry `SecretWhenUnitIdentityRestricted`. In practice that means "secret in PvP, in restricted instances, and for non-player or pet units in combat". Your code works fine right up until it does not.
218
291
 
219
- Under the default `--patch=12.1`, the seven identity APIs WSL013 covers are promoted out of this tier and report as errors regardless of the `--conditional` setting. The rest of the tier is **off by default**, because measured against 12 real addons it produces about one warning per Lua file, which is a tax rather than a signal. Turn it on for a deeper audit:
292
+ From `--patch=12.1` on, the identity APIs WSL013 covers are promoted out of this tier and report as errors regardless of the `--conditional` setting. The rest of the tier is **off by default**, because measured against 12 real addons it produces about one warning per Lua file, which is a tax rather than a signal. Turn it on for a deeper audit:
220
293
 
221
294
  ```bash
222
295
  npx wow-secret-lint ./MyAddon --conditional=warn # report as warnings
@@ -249,12 +322,13 @@ Run against 12 real retail addons (BigWigs, LittleWigs, DBM, WeakAuras, Details,
249
322
 
250
323
  | Mode | Errors | Warnings | Addons that would fail CI |
251
324
  | --- | --- | --- | --- |
252
- | `--patch=12.1` (default) | 379 | 121 | 9 of 12 |
325
+ | `--patch=12.1.5` (default) | 379 | 121 | 9 of 12 |
326
+ | `--patch=12.1` | 379 | 121 | 9 of 12 |
253
327
  | `--patch=12.1 --strict` | 489 | 11 | 9 of 12 |
254
328
  | `--patch=12.0` | 20 | 90 | 3 of 12 |
255
329
  | `--patch=12.0 --strict` | 110 | 0 | 5 of 12 |
256
330
 
257
- The two `12.0` rows are identical to what v1.2.0 reported, which is the point of the flag. Everything above them is the 12.1 surface landing, concentrated exactly where the [PTR forum thread](https://us.forums.blizzard.com/en/wow/t/minicc-and-similar-addons-might-be-partially-broken-in-121/2310937) predicted breakage:
331
+ The two `12.0` rows are identical to what v1.2.0 reported, and the `12.1` rows to what v1.4.2 reported, which is the point of the flag. Everything above them is the 12.1 surface landing, concentrated exactly where the [PTR forum thread](https://us.forums.blizzard.com/en/wow/t/minicc-and-similar-addons-might-be-partially-broken-in-121/2310937) predicted breakage:
258
332
 
259
333
  | Rule | Count | Where |
260
334
  | --- | --- | --- |
@@ -264,9 +338,12 @@ The two `12.0` rows are identical to what v1.2.0 reported, which is the point of
264
338
  | WSL014 | 10 | mostly `GetInventorySlotInfo`, plus `GetInspectSpecialization`, `GetWeaponEnchantInfo` and SpartanUI's `UIParentLoadAddOn` |
265
339
  | WSL016 | 3 | oUF `privateauras.lua:133` and both copies vendored into KkthnxUI and SpartanUI |
266
340
  | WSL017 | 0 | no addon in the corpus uses AuraContainers yet; exercised by fixtures |
341
+ | WSL019, WSL020, WSL021 | 0 | see below |
267
342
 
268
343
  Two numbers are worth reading together. **DBM and BigWigs report no WSL012 at all**, because both already wrap every aura lookup in an `issecretvalue` guard and the analysis credits that idiom. **BigWigs still has four WSL018 findings**, because guarding the result does not help when the call itself is what errors. That pair is the honest summary of where the ecosystem is: the careful addons did the guard work, and the guard work is not enough.
269
344
 
345
+ The three 12.1.5 rules report **nothing** on this corpus, and nothing on a second pass on 2026-09-07 over each addon's current HEAD plus OmniCC, Dominos and Blizzard's own interface code: 4,218 more files, zero findings. Four days after the patch, no addon in reach has adopted the Pandemic animation APIs, and none of them drives Blizzard's action-button cooldowns in a way a static reader can see. OmniCC gets closest and is the honest limit of the rule: it hooks `SetCooldown` on `getmetatable(ActionButton1Cooldown).__index` and dispatches through a proxy with the method name as a string, so the frame is never named at the call site. WSL021 sees a protected cooldown only where the file names one.
346
+
270
347
  A sample of the WSL012/WSL013 findings across every affected repo was read against its source by hand; each one performs an operation the 12.1 notes document as disallowed, on a value those notes document as secret, with no guard in scope. The pre-12.1 findings are unchanged from v1.2.0, where all 110 strict findings were read by hand.
271
348
 
272
349
  Contamination from Classic-flavour code is 0, checked by resolving every retail `.toc` in each repo and matching it against the findings.
@@ -295,8 +372,8 @@ Settling it needs someone to run a flagged line in game and watch what happens,
295
372
  ```
296
373
  --format=<stylish|json|github> output format (default: stylish)
297
374
  --game=<retail|classic> classic has no secret values and exits 0 immediately
298
- --patch=<12.0|12.1> patch surface the built-in rules check (default: 12.1);
299
- 12.0 pins the pre-12.1 rule set
375
+ --patch=<12.0|12.1|12.1.5|auto> patch surface the built-in rules check (default: 12.1.5);
376
+ an older value pins that rule set, auto reads the addon .toc
300
377
  --strict raise SecretReturns findings from warning to error
301
378
  --conditional=<off|warn|error> conditionally secret APIs (default: off)
302
379
  --secret-guard=<names> extra is-secret wrapper functions, comma separated
@@ -305,12 +382,36 @@ Settling it needs someone to run a flagged line in game and watch what happens,
305
382
  --max-warnings=<n> exit 1 when warnings exceed n
306
383
  --snapshot=<path> use a different API snapshot
307
384
  --refresh rebuild the vendored API snapshot (the only networked command)
385
+ --refresh-ref=<ref> branch or tag of the mirror to rebuild from (default: live)
386
+ --force let --refresh write an older client build than the vendored one
308
387
  --rules print the rule table with its sources
309
388
  ```
310
389
 
311
390
  ## Where the data comes from
312
391
 
313
- `data/api-snapshot.json` is built from Blizzard's generated API documentation, mirrored at [Gethe/wow-ui-source](https://github.com/Gethe/wow-ui-source) on the `live` branch. The current snapshot carries **10,098 documented functions and 752 structures**: 20 with `SecretReturns = true`, 310 conditionally secret, and per-field `NeverSecret` markers on 20 structures.
392
+ `data/api-snapshot.json` is built from Blizzard's generated API documentation, mirrored at [Gethe/wow-ui-source](https://github.com/Gethe/wow-ui-source). The current snapshot is patch **12.1.5, build 69594**, rebuilt from that mirror's `12.1.5` tag, and carries **10,250 documented functions and 760 structures**: 20 with `SecretReturns = true`, 314 conditionally secret, and per-field `NeverSecret` markers on 20 structures. It also carries the widget markers the 12.1.5 rules read: `ChecksForbiddenAspects` and `IsProtectedFunction`, keyed by widget system because method names collide across widget types.
393
+
394
+ This is the half of the release with no new rules in it and it still changes results. WSL006 and WSL007 consult the snapshot before they fire, so an addon that has already adopted a 12.1.5 API was being analysed against a documentation set that had never heard of the symbol:
395
+
396
+ ```
397
+ $ npx wow-secret-lint --strict --snapshot=./api-snapshot-1.4.2.json Adopter.lua
398
+ 0 errors, 0 warnings
399
+
400
+ $ npx wow-secret-lint --strict Adopter.lua
401
+ Adopter.lua:2:24 error WSL006 secret value passed to math.clamp(), which is documented SecretArguments = "AllowedWhenUntainted" and addon code is always tainted: 'hp' derives from UnitHealth() (SecretReturns=true)
402
+ Adopter.lua:3:29 error WSL006 secret value passed to C_Intl.ToUpper(), which is documented SecretArguments = "AllowedWhenUntainted" and addon code is always tainted: derives from UnitSpellTargetName() (SecretReturns=true)
403
+ Adopter.lua:4:22 error WSL006 secret value passed to string.startswith(), which is documented SecretArguments = "AllowedWhenUntainted" and addon code is always tainted: derives from UnitSpellTargetName() (SecretReturns=true)
404
+ 3 errors, 0 warnings
405
+ ```
406
+
407
+ The refresh also picked up a new conditional marker with no code change needed: `table.count`, `table.getcountinfo` and `table.isempty` carry `SecretWhenLuaTableHasSecretKeys`, so `--conditional=warn` reports them like any other `SecretWhen*` API.
408
+
409
+ Every count on this page comes out of the snapshot itself:
410
+
411
+ ```bash
412
+ node -p "const s=require('./data/api-snapshot.json'); [s.patch, s.build, s.functionCount, s.structureCount, s.secretReturnCount, s.conditionalCount, s.annotatedStructureCount].join(' ')"
413
+ 12.1.5 69594 10250 760 20 314 20
414
+ ```
314
415
 
315
416
  It is parsed with `luaparse`, not regexed, so nested tables and multi-line entries cannot skew it. Rebuild it any time:
316
417
 
@@ -320,7 +421,22 @@ npx wow-secret-lint --refresh
320
421
 
321
422
  A scheduled workflow in this repo does that weekly and opens a pull request when the docs move.
322
423
 
323
- The 12.1 aura and identity secrecy is deliberately **not** part of the snapshot: Blizzard's generated documentation carries no marker for it, only the patch notes describe it, so the WSL012/WSL013 API lists live in `src/rules.mjs` with the source quoted, and `--patch` gates them. There is one snapshot, and `--snapshot`/`--refresh` behave the same under either patch.
424
+ The mirror tags a client build before it moves its `live` branch, which is how 12.1.5 was picked up here on the day it shipped:
425
+
426
+ ```bash
427
+ npx wow-secret-lint --refresh --refresh-ref=12.1.5
428
+ ```
429
+
430
+ The snapshot records the patch and build it came from, read off the mirror's own commit message, and `--refresh` will not walk it backwards onto an older build without `--force`. Point it at a stale ref and it says so and leaves the file alone:
431
+
432
+ ```
433
+ $ npx wow-secret-lint --refresh --refresh-ref=12.1.0
434
+ rebuilding API snapshot from Gethe/wow-ui-source@12.1.0 ...
435
+ wow-secret-lint: @12.1.0 is still 12.1.0 (build 69587), older than the vendored 12.1.5
436
+ (build 69594); left the snapshot alone. Pass --force to write it anyway.
437
+ ```
438
+
439
+ The 12.1 aura and identity secrecy is deliberately **not** part of the snapshot: Blizzard's generated documentation carries no marker for it, only the patch notes describe it, so the WSL012/WSL013 API lists live in `src/rules.mjs` with the source quoted, and `--patch` gates them. The 12.1.5 rules are the other way round. Blizzard does mark those in the generated docs, so WSL019 and WSL020 read their method lists out of the snapshot and WSL021 reads the six `IsProtectedFunction` cooldown methods out of it; only the question of *which objects* carry the aspects lives in `src/rules.mjs`. There is one snapshot, and `--snapshot`/`--refresh` behave the same under every patch.
324
440
 
325
441
  ## Limitations and non-goals
326
442
 
@@ -330,7 +446,7 @@ The 12.1 aura and identity secrecy is deliberately **not** part of the snapshot:
330
446
  - **It cannot diagnose a taint-spread crash.** Some of the worst reports look like `attempt to perform arithmetic on local 'textHeight' (a secret number value, while execution tainted by 'YourAddon')` where every frame of the stack is a Blizzard file. Your addon tainted execution, and then Blizzard's code did the arithmetic. There is no forbidden operation in your Lua to point at, so this tool reports nothing. Running it over the real [aura-questor](https://github.com/lucascodev/aura-questor) source, which has exactly that open report, gives a clean run across 114 files. The regression fixture named after that issue reproduces the shape of the trace, not a finding in their code.
331
447
  - **No cross-file interprocedural analysis in v1.** Taint follows plain assignment, table field stores, the return value of a file-local function, and one level of intra-file call-argument passing. A secret that leaves through a global and comes back in another file is not tracked.
332
448
  - **No LuaJIT or Lua 5.4 syntax.** Files are parsed as Lua 5.1. WoW accepts a semicolon after `break`, which stock 5.1 does not, so a file that fails on 5.1 gets one retry under the 5.2 grammar before it is reported as a parse error.
333
- - **Method calls are not resolved to a widget type**, so WSL006 only applies to plain and namespaced calls (`UnitHealth(...)`, `C_CVar.SetCVar(...)`), never to `frame:SetText(...)`. WSL017 is the one exception, and its typing is deliberately narrow: a local counts as an AuraContainer/AuraButton only when it comes straight from `CreateFrame` with a literal type string or from an `initializeFrame` callback. A button stored in a table field or passed across files is not tracked.
449
+ - **Method calls are not resolved to a widget type**, so WSL006 only applies to plain and namespaced calls (`UnitHealth(...)`, `C_CVar.SetCVar(...)`), never to `frame:SetText(...)`. WSL017 and WSL019-WSL021 are the exceptions, and their typing is deliberately narrow: a local counts as an AuraContainer/AuraButton only when it comes straight from `CreateFrame` with a literal type string or from an `initializeFrame` callback, an animation group carries the 12.1.5 aspects only from the `AddPandemic*Animation` call that registers it, and a cooldown is protected only where the file names Blizzard's frame or builds one from a secure template. Values that arrive across files, through a metatable hook, or through a proxy that takes the method name as a string are not tracked.
334
450
  - **XML is followed for `<Script>`/`<Include>` and checked only for removed templates.** WSL014 reads `inherits` attributes in every `.xml` the `.toc` pulls in, so a declarative aura header is caught. Nothing else in the markup is analysed: widget scripts written inline in XML are not parsed as Lua.
335
451
  - **`string.format` output is not tracked as secret.** The wiki names it as the sanctioned way to render a secret, and following it would flood every `SetText` call site. The trade is a known blind spot on `#string.format(...)`.
336
452
  - A `.toc` listing a file that is not on disk warns and keeps going. A file that will not parse is reported and the run exits 2.
@@ -341,7 +457,7 @@ The 12.1 aura and identity secrecy is deliberately **not** part of the snapshot:
341
457
  npm test
342
458
  ```
343
459
 
344
- 181 tests. The suite covers every rule, the guard forms, the permitted-operations negative cases, the three reporters, the CLI surface, one violating and one clean fixture per 12.1 rule (`test/fixtures/rules-121/`), a fixture proving `--patch=12.0` reproduces the v1.2.0 output byte for byte (`test/fixtures/patch/`), and eight regression fixtures reconstructed from real shipped traces:
460
+ 230 tests. The suite covers every rule, the guard forms, the permitted-operations negative cases, the three reporters, the CLI surface, one violating and one clean fixture per 12.1 and 12.1.5 rule (`test/fixtures/rules-121/`, `test/fixtures/rules-1215/`), recorded baselines proving `--patch=12.0` reproduces the v1.2.0 output and `--patch=12.1` the v1.4.2 output byte for byte over the whole fixture corpus (`test/fixtures/patch/`), and eight regression fixtures reconstructed from real shipped traces:
345
461
 
346
462
  | Fixture | Issue |
347
463
  | --- | --- |
package/action/index.mjs CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
  import process from 'node:process';
9
9
  import { lintPaths, VERSION } from '../src/index.mjs';
10
10
  import { format, FORMATS, counts } from '../src/report.mjs';
11
- import { RULE_IDS, PATCHES, DEFAULT_PATCH } from '../src/rules.mjs';
11
+ import { RULE_IDS, PATCHES, DEFAULT_PATCH, patchList } from '../src/rules.mjs';
12
12
 
13
13
  const HERE = dirname(fileURLToPath(import.meta.url));
14
14
 
@@ -105,8 +105,8 @@ if (!['off', 'warn', 'error'].includes(options.conditional)) {
105
105
  if (!['retail', 'classic'].includes(options.game)) {
106
106
  fail(`unknown --game "${options.game}" (expected retail or classic)`);
107
107
  }
108
- if (!PATCHES.includes(options.patch)) {
109
- fail(`unknown patch "${options.patch}" (expected ${PATCHES.join(' or ')})`);
108
+ if (options.patch !== 'auto' && !PATCHES.includes(options.patch)) {
109
+ fail(`unknown patch "${options.patch}" (expected ${patchList()}, or auto)`);
110
110
  }
111
111
  for (const id of options.disable) {
112
112
  if (!RULE_IDS.includes(id)) fail(`unknown rule id "${id}" in --disable`);
@@ -142,7 +142,7 @@ const rows = merged.findings
142
142
 
143
143
  await summary(
144
144
  `## wow-secret-lint ${VERSION}\n\n` +
145
- `${merged.filesScanned} Lua file(s) scanned against ${merged.snapshot.functionCount ?? 0} documented APIs ` +
145
+ `${merged.filesScanned} Lua file(s) scanned on the ${merged.patch} surface against ${merged.snapshot.functionCount ?? 0} documented APIs ` +
146
146
  `(${merged.snapshot.secretReturnCount ?? 0} with \`SecretReturns=true\`).\n\n` +
147
147
  `**${errors} error(s), ${warnings} warning(s)**` +
148
148
  (merged.parseErrors.length ? `, ${merged.parseErrors.length} parse error(s)` : '') +
package/action.yml CHANGED
@@ -11,9 +11,9 @@ inputs:
11
11
  required: false
12
12
  default: '.'
13
13
  patch:
14
- description: 'Patch surface to check: 12.1 (default) or 12.0 for addons pinned to the older client.'
14
+ description: 'Patch surface to check: 12.1.5 (default), 12.1, 12.0, or auto to read the addon .toc.'
15
15
  required: false
16
- default: '12.1'
16
+ default: '12.1.5'
17
17
  args:
18
18
  description: 'Extra CLI arguments, e.g. "--conditional=warn --disable=WSL011".'
19
19
  required: false
@@ -6,8 +6,8 @@
6
6
  import process from 'node:process';
7
7
  import { lintPaths, VERSION } from '../src/index.mjs';
8
8
  import { format, FORMATS, counts } from '../src/report.mjs';
9
- import { refreshSnapshot, writeSnapshot, SNAPSHOT_PATH } from '../src/apidata.mjs';
10
- import { RULES, RULE_IDS, PATCHES, DEFAULT_PATCH } from '../src/rules.mjs';
9
+ import { refreshSnapshot, writeSnapshot, loadSnapshot, SNAPSHOT_PATH, DEFAULT_REF } from '../src/apidata.mjs';
10
+ import { RULES, RULE_IDS, PATCHES, DEFAULT_PATCH, patchList, patchAtLeast } from '../src/rules.mjs';
11
11
 
12
12
  const USAGE = `wow-secret-lint ${VERSION}
13
13
  Static analysis for World of Warcraft retail addons: finds Secret Value violations
@@ -21,9 +21,10 @@ Usage:
21
21
  Options:
22
22
  --format=<stylish|json|github> output format (default: stylish)
23
23
  --game=<retail|classic> classic has no secret values and exits 0 immediately
24
- --patch=<12.0|12.1> which patch surface the built-in rules check
25
- (default: 12.1). 12.0 pins the pre-12.1 rule set for
26
- addons still targeting the older client
24
+ --patch=<12.0|12.1|12.1.5|auto> which patch surface the built-in rules check
25
+ (default: 12.1.5). An older value pins the rule set for
26
+ addons still targeting an older client, byte for byte;
27
+ auto reads the addon's own .toc Interface number
27
28
  --strict raise SecretReturns findings from warning to error.
28
29
  Off by default: see "the open question on severity"
29
30
  in the README before you gate CI on them.
@@ -38,6 +39,10 @@ Options:
38
39
  --snapshot=<path> use a different API snapshot
39
40
  --refresh rebuild the vendored API snapshot from the public mirror
40
41
  (the only command that uses the network)
42
+ --refresh-ref=<ref> branch or tag of the mirror to rebuild from
43
+ (default: ${DEFAULT_REF})
44
+ --force let --refresh write a snapshot of an older client build
45
+ than the one already vendored
41
46
  --rules print the rule table and exit
42
47
  --version print the version and exit
43
48
  -h, --help print this help and exit
@@ -64,6 +69,8 @@ function parseArgs(argv) {
64
69
  maxWarnings: Infinity,
65
70
  snapshot: undefined,
66
71
  refresh: false,
72
+ refreshRef: DEFAULT_REF,
73
+ force: false,
67
74
  rules: false,
68
75
  help: false,
69
76
  version: false,
@@ -78,6 +85,7 @@ function parseArgs(argv) {
78
85
  if (arg === '-h' || arg === '--help') opts.help = true;
79
86
  else if (arg === '--version' || arg === '-v') opts.version = true;
80
87
  else if (arg === '--refresh') opts.refresh = true;
88
+ else if (arg === '--force') opts.force = true;
81
89
  else if (arg === '--rules') opts.rules = true;
82
90
  else if (arg === '--strict') opts.strict = true;
83
91
  else if (arg.startsWith('--format')) opts.format = value(arg, argv, () => i++);
@@ -88,6 +96,7 @@ function parseArgs(argv) {
88
96
  else if (arg.startsWith('--access-guard')) opts.accessGuards.push(...value(arg, argv, () => i++).split(',').map((s) => s.trim()).filter(Boolean));
89
97
  else if (arg.startsWith('--disable')) opts.disable = value(arg, argv, () => i++).split(',').map((s) => s.trim()).filter(Boolean);
90
98
  else if (arg.startsWith('--max-warnings')) opts.maxWarnings = Number(value(arg, argv, () => i++));
99
+ else if (arg.startsWith('--refresh-ref')) opts.refreshRef = value(arg, argv, () => i++);
91
100
  else if (arg.startsWith('--snapshot')) opts.snapshot = value(arg, argv, () => i++);
92
101
  else if (arg.startsWith('-')) throw new Error(`unknown option "${arg}"`);
93
102
  else opts.paths.push(arg);
@@ -129,11 +138,12 @@ async function main() {
129
138
  }
130
139
 
131
140
  if (opts.refresh) {
132
- process.stderr.write('rebuilding API snapshot from Gethe/wow-ui-source@live ...\n');
141
+ process.stderr.write(`rebuilding API snapshot from Gethe/wow-ui-source@${opts.refreshRef} ...\n`);
133
142
  let index;
134
143
  try {
135
144
  let last = 0;
136
145
  index = await refreshSnapshot({
146
+ ref: opts.refreshRef,
137
147
  onProgress: ({ done, total }) => {
138
148
  if (done - last >= 50 || done === total) {
139
149
  last = done;
@@ -144,9 +154,22 @@ async function main() {
144
154
  } catch (err) {
145
155
  fail(`--refresh failed: ${err.message}`);
146
156
  }
147
- const path = await writeSnapshot(index, opts.snapshot ?? SNAPSHOT_PATH);
157
+ const path = opts.snapshot ?? SNAPSHOT_PATH;
158
+ const vendored = await loadSnapshot(path).catch(() => null);
159
+ // The mirror tags a new patch before it moves the live branch, so a scheduled refresh
160
+ // can otherwise walk the snapshot back onto a client build that is no longer current.
161
+ if (!opts.force && vendored && vendored.build && index.build && index.build < vendored.build) {
162
+ process.stderr.write(
163
+ `wow-secret-lint: @${opts.refreshRef} is still ${index.patch ?? '?'} (build ${index.build}), older than ` +
164
+ `the vendored ${vendored.patch ?? '?'} (build ${vendored.build}); left the snapshot alone. ` +
165
+ `Pass --force to write it anyway.\n`
166
+ );
167
+ return 0;
168
+ }
169
+ await writeSnapshot(index, path);
148
170
  process.stderr.write(
149
- `wrote ${path}: ${index.functionCount} functions, ${index.secretReturnCount} with SecretReturns=true, ` +
171
+ `wrote ${path}: patch ${index.patch ?? 'unknown'} build ${index.build ?? 'unknown'}, ` +
172
+ `${index.functionCount} functions, ${index.secretReturnCount} with SecretReturns=true, ` +
150
173
  `${index.conditionalCount} conditionally secret, ${index.structureCount} structures\n`
151
174
  );
152
175
  if (index.failures && index.failures.length) {
@@ -157,7 +180,9 @@ async function main() {
157
180
 
158
181
  if (!FORMATS.includes(opts.format)) fail(`unknown format "${opts.format}" (expected one of: ${FORMATS.join(', ')})`);
159
182
  if (!['retail', 'classic'].includes(opts.game)) fail(`unknown game "${opts.game}" (expected retail or classic)`);
160
- if (!PATCHES.includes(opts.patch)) fail(`unknown --patch "${opts.patch}" (expected ${PATCHES.join(' or ')})`);
183
+ if (opts.patch !== 'auto' && !PATCHES.includes(opts.patch)) {
184
+ fail(`unknown --patch "${opts.patch}" (expected ${patchList()}, or auto)`);
185
+ }
161
186
  if (!['warn', 'error', 'off'].includes(opts.conditional)) {
162
187
  fail(`unknown --conditional "${opts.conditional}" (expected warn, error or off)`);
163
188
  }
@@ -177,6 +202,14 @@ async function main() {
177
202
  return 0;
178
203
  }
179
204
 
205
+ const snapshot = await loadSnapshot(opts.snapshot).catch(() => null);
206
+ if (snapshot && snapshot.patch && opts.patch !== 'auto' && !patchAtLeast(snapshot.patch, opts.patch)) {
207
+ process.stderr.write(
208
+ `wow-secret-lint: the vendored snapshot is patch ${snapshot.patch} but --patch is ${opts.patch}; ` +
209
+ `the rules that read the snapshot are checking an older API surface. Run --refresh.\n`
210
+ );
211
+ }
212
+
180
213
  let merged;
181
214
  try {
182
215
  merged = await lintPaths(opts.paths, {