forever-lint 0.1.0__tar.gz

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.
Files changed (41) hide show
  1. forever_lint-0.1.0/.gitignore +11 -0
  2. forever_lint-0.1.0/CHANGELOG.md +11 -0
  3. forever_lint-0.1.0/LICENSE +21 -0
  4. forever_lint-0.1.0/PKG-INFO +181 -0
  5. forever_lint-0.1.0/README.es.md +84 -0
  6. forever_lint-0.1.0/README.md +161 -0
  7. forever_lint-0.1.0/action.yml +38 -0
  8. forever_lint-0.1.0/pyproject.toml +49 -0
  9. forever_lint-0.1.0/src/forever_lint/__init__.py +3 -0
  10. forever_lint-0.1.0/src/forever_lint/__main__.py +3 -0
  11. forever_lint-0.1.0/src/forever_lint/analyzer.py +882 -0
  12. forever_lint-0.1.0/src/forever_lint/cli.py +180 -0
  13. forever_lint-0.1.0/src/forever_lint/data/api.json +58444 -0
  14. forever_lint-0.1.0/src/forever_lint/db.py +71 -0
  15. forever_lint-0.1.0/src/forever_lint/lexer.py +170 -0
  16. forever_lint-0.1.0/src/forever_lint/luatable.py +98 -0
  17. forever_lint-0.1.0/src/forever_lint/report.py +222 -0
  18. forever_lint-0.1.0/src/forever_lint/toc.py +211 -0
  19. forever_lint-0.1.0/tests/fixtures/ClassicOnly/ClassicOnly_Vanilla.toc +3 -0
  20. forever_lint-0.1.0/tests/fixtures/ClassicOnly/Main.lua +1 -0
  21. forever_lint-0.1.0/tests/fixtures/Excluded/Excluded.toc +3 -0
  22. forever_lint-0.1.0/tests/fixtures/Excluded/Main.lua +1 -0
  23. forever_lint-0.1.0/tests/fixtures/Ported/Classic.lua +1 -0
  24. forever_lint-0.1.0/tests/fixtures/Ported/Core.lua +18 -0
  25. forever_lint-0.1.0/tests/fixtures/Ported/Libs/LibFoo/LibFoo.lua +1 -0
  26. forever_lint-0.1.0/tests/fixtures/Ported/Locale.enUS.lua +1 -0
  27. forever_lint-0.1.0/tests/fixtures/Ported/Ported.toc +2 -0
  28. forever_lint-0.1.0/tests/fixtures/Ported/Ported_Camelot.toc +7 -0
  29. forever_lint-0.1.0/tests/fixtures/Ported/Retail.lua +1 -0
  30. forever_lint-0.1.0/tests/fixtures/Sample/Core.lua +30 -0
  31. forever_lint-0.1.0/tests/fixtures/Sample/Frames.lua +1 -0
  32. forever_lint-0.1.0/tests/fixtures/Sample/Sample.toc +4 -0
  33. forever_lint-0.1.0/tests/fixtures/Sample/Sample_Vanilla.toc +3 -0
  34. forever_lint-0.1.0/tests/fixtures/Sample/UI.xml +1 -0
  35. forever_lint-0.1.0/tests/test_analyzer.py +253 -0
  36. forever_lint-0.1.0/tests/test_feed.py +66 -0
  37. forever_lint-0.1.0/tests/test_lexer.py +70 -0
  38. forever_lint-0.1.0/tests/test_toc_cli.py +136 -0
  39. forever_lint-0.1.0/tools/build_db.py +389 -0
  40. forever_lint-0.1.0/tools/curated.json +50 -0
  41. forever_lint-0.1.0/tools/history-names.txt +19705 -0
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ build/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ vendor/
8
+ *.sarif
9
+ summary.txt
10
+ .index-cache/
11
+ site/
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-03)
4
+
5
+ First release, against WoW Forever 1.60.1 (70532) and its UI source 1.60.1 (70755).
6
+
7
+ - Rules from the 1.60.1 API notes (warcraft.wiki, Oct 2026): `require` renamed to `import` (FL001), `GetFrameStrata` secret values (FL003), SAY/YELL/EMOTE hardware events (FL011), trial accounts and addon messages (FL012).
8
+ - Rules FL001–FL010: removed and deprecated API, secret values, `WOW_PROJECT_ID`, `GetBuildInfo` version checks, removed and restricted events, TOC selection, interface version and game-type exclusions.
9
+ - Follows the TOC Forever loads (game-type suffixes, per-file directives, XML includes).
10
+ - Scope-aware analysis with polyfill, guard and checked-replacement detection.
11
+ - Text, JSON and SARIF output; `--explain`; inline suppressions; GitHub Action.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 José Argento
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.5
2
+ Name: forever-lint
3
+ Version: 0.1.0
4
+ Summary: Checks World of Warcraft addons for problems on WoW Forever: removed API, secret values, project ID checks, events and TOC files.
5
+ Project-URL: Homepage, https://github.com/JoseArgento/forever-lint
6
+ Project-URL: Issues, https://github.com/JoseArgento/forever-lint/issues
7
+ Author: José Argento
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: addon,lint,lua,sarif,static-analysis,world-of-warcraft,wow
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Lua
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Games/Entertainment
17
+ Classifier: Topic :: Software Development :: Quality Assurance
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+
21
+ # forever-lint
22
+
23
+ Checks World of Warcraft addons for problems on **WoW Forever**, before your users find them.
24
+
25
+ Forever looks like Classic but runs the modern client: Classic-era version numbers on top of the retail API, with Midnight's addon restrictions. Code written for Classic Era breaks in ways that are easy to miss: `GetSpellInfo` is gone, `WOW_PROJECT_ID` is neither Classic nor retail, health values can be secret, and an addon with only a `_Vanilla.toc` does not load at all.
26
+
27
+ ```
28
+ $ forever-lint MyAddon
29
+ forever-lint 0.1.0 · WoW Forever 1.60.1 (70532), interface 16001
30
+
31
+ MyAddon (MyAddon, MyAddon.toc, 2 Lua files)
32
+ warning FL009 MyAddon.toc declares Interface 11509, 120105 but Forever is 16001: the addon shows as out of date ...
33
+
34
+ MyAddon/Core.lua
35
+ 2:22 error FL001 `GetSpellInfo` does not exist in Forever. Probably replaced by `C_Spell.GetSpellInfo`. Note: `C_Spell.GetSpellInfo` returns a SpellInfo table, not separate values.
36
+ 4:19 warning FL004 In Forever `WOW_PROJECT_ID` is 18 (`WOW_PROJECT_CAMELOT`), so `WOW_PROJECT_ID == WOW_PROJECT_CLASSIC` is false. ...
37
+ 10:10 warning FL003 `hp` holds the result of `UnitHealth`, which can return a secret value in Forever (always). ...
38
+ 14:17 warning FL006 Event `COMBAT_LOG_EVENT_UNFILTERED`: the combat log is restricted for addons in Forever ...
39
+ 18:16 error FL001 `GetItemInfo` does not exist in Forever. Use `C_Item.GetItemInfo`.
40
+
41
+ 8 errors, 6 warnings, 1 note in 1 addon(s)
42
+ ```
43
+
44
+ [Leer en castellano](README.es.md)
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install forever-lint # Python 3.10+, no dependencies
50
+ forever-lint path/to/MyAddon # one addon, a repo with several, or your whole AddOns folder
51
+ ```
52
+
53
+ ## What it checks
54
+
55
+ | Rule | Severity | What |
56
+ |---|---|---|
57
+ | <a id="fl001-removed-api"></a>**FL001** removed-api | error | A function, namespace or Blizzard UI function that does not exist in Forever, with its replacement when there is one. Also `hooksecurefunc("Name", ...)` on a missing global, which raises an error. |
58
+ | <a id="fl002-deprecated-api"></a>**FL002** deprecated-api | warning | API that only exists as one of Blizzard's deprecation fallbacks (loaded while the `loadDeprecationFallbacks` CVar is on). Blizzard says these will be removed. |
59
+ | <a id="fl003-secret-value"></a>**FL003** secret-value | warning | Arithmetic or comparisons on the result of a function documented to return secret values (`UnitHealth`, `UnitPower` when restricted, `frame:GetFrameStrata()` since 1.60.1, ...). In addon code that raises a Lua error. |
60
+ | <a id="fl004-project-id"></a>**FL004** project-id | warning | `WOW_PROJECT_ID` comparisons, with what they evaluate to in Forever (18, `WOW_PROJECT_CAMELOT`). Lines that already mention `WOW_PROJECT_CAMELOT` are notes. |
61
+ | <a id="fl005-removed-event"></a>**FL005** removed-event | error | `RegisterEvent("EVENT")` for an event Forever does not have. |
62
+ | <a id="fl006-restricted-event"></a>**FL006** restricted-event | warning | Events with restrictions or secret payloads, such as `COMBAT_LOG_EVENT_UNFILTERED`. |
63
+ | <a id="fl007-build-version"></a>**FL007** build-version | warning | `select(4, GetBuildInfo())` checks: Forever reports 16001, so "modern client" tests like `>= 100000` are false. |
64
+ | <a id="fl008-no-forever-toc"></a>**FL008** no-forever-toc | error | No TOC that Forever loads. It looks for `_Camelot`, `_Forever`, `_Mainline`, then the plain `.toc`. |
65
+ | <a id="fl009-toc-interface"></a>**FL009** toc-interface | warning | The TOC's `## Interface:` does not list 16001, so the addon shows as out of date. |
66
+ | <a id="fl010-toc-game-type"></a>**FL010** toc-game-type | note | The TOC excludes Forever with `AllowLoadGameType`/`ExcludeLoadGameType`. Fine if on purpose. |
67
+ | <a id="fl011-chat-hardware-event"></a>**FL011** chat-hardware-event | note | `SendChatMessage` with SAY, YELL or EMOTE: outside instances it needs a key press or click (EMOTE since 1.60.1). Inside instances a beta tester reported it no longer works at all (18/10, not confirmed). |
68
+ | <a id="fl012-trial-addon-messages"></a>**FL012** trial-addon-messages | note | Trial accounts cannot send addon messages (1.60.1). Reported once per addon. |
69
+ | <a id="fl013-beta-event-quirk"></a>**FL013** beta-event-quirk | note | Events an addon author reported behave differently in Forever's beta (18/10, not confirmed): `PLAYER_DEAD` and the `MERCHANT_CLOSED`/`AUCTION_HOUSE_CLOSED` pair fire twice, and the zone text right after `ZONE_CHANGED_NEW_AREA` can be stale or empty after travelling. |
70
+
71
+ It follows the TOC Forever would pick, including `[AllowLoadGameType ...]` per file and the `<Script>`/`<Include>` files of XML, so code for other clients is not reported. `--all-files` checks every `.lua` file instead.
72
+
73
+ ### Fewer false positives
74
+
75
+ The analysis tracks Lua scopes, so a local or parameter called `GetSpellInfo` is not reported. It also recognises the usual multi-client patterns:
76
+
77
+ - **Polyfills**: if the addon assigns the global itself (`GetSpellInfo = GetSpellInfo or function ...`), uses anywhere in the addon are fine.
78
+ - **Guards**: `if GetMouseFocus then`, `GetMouseFocus and ...`, `C_X and C_X.Y or Old` become notes, because the fallback branch is the one that runs in Forever.
79
+ - **Checked replacement**: `if C_Item and C_Item.GetItemSpell then return C_Item.GetItemSpell(id) end return GetItemSpell(id)` is a note. Forever has the new function, so the old line does not run there.
80
+ - **Embedded libraries** (`Libs/`, `lib/`, ...) are summarised, never fail the run, and are listed with `--libs`.
81
+
82
+ ### Silencing a finding
83
+
84
+ ```lua
85
+ local x = GetMouseFocus() -- forever-lint: ignore FL001
86
+ -- forever-lint: ignore-next-line
87
+ OldThing()
88
+ -- forever-lint: ignore-file
89
+ ```
90
+
91
+ Or skip whole rules with `--ignore FL002,FL004`.
92
+
93
+ ## With luacheck or pre-commit
94
+
95
+ `forever-lint --export-luacheck > .luacheckrc.forever` prints a `read_globals` block with every function Forever has (deprecation fallbacks left out), to paste into an existing `.luacheckrc`.
96
+
97
+ For [pre-commit](https://pre-commit.com):
98
+
99
+ ```yaml
100
+ repos:
101
+ - repo: https://github.com/JoseArgento/forever-lint
102
+ rev: v0.1.0
103
+ hooks:
104
+ - id: forever-lint
105
+ ```
106
+
107
+ ## Options
108
+
109
+ ```
110
+ forever-lint PATH... [--format text|json|sarif] [-o FILE] [--fail-on error|warning|note|never]
111
+ [--ignore RULES] [--libs] [--all-files] [--no-color]
112
+ forever-lint --explain GetSpellInfo # what Forever has for a name
113
+ forever-lint --list-rules
114
+ ```
115
+
116
+ The exit code is 1 when there is a finding at the `--fail-on` severity (default `error`) outside libraries.
117
+
118
+ ## In CI (GitHub Actions)
119
+
120
+ Findings show up as code scanning annotations on the pull request:
121
+
122
+ ```yaml
123
+ jobs:
124
+ forever:
125
+ runs-on: ubuntu-latest
126
+ permissions:
127
+ contents: read
128
+ security-events: write
129
+ steps:
130
+ - uses: actions/checkout@v4
131
+ - uses: JoseArgento/forever-lint@v0
132
+ with:
133
+ path: .
134
+ fail-on: error
135
+ ```
136
+
137
+ Without the action: `pip install forever-lint && forever-lint . --format sarif -o forever-lint.sarif`, then upload the file with `github/codeql-action/upload-sarif`.
138
+
139
+ ## Compatibility index
140
+
141
+ `index/` builds a static site that runs forever-lint over popular addons with public GitHub repositories (`index/addons.json`) and publishes the results with GitHub Pages every week. It shows file, line and message for every finding, never the addons' code. To add an addon, open a pull request with its repository in `index/addons.json`.
142
+
143
+ ```bash
144
+ python index/build_index.py --cache .index-cache --out site # then open site/index.html
145
+ python index/build_api.py --out site # site/api.html: every Classic API that moved
146
+ ```
147
+
148
+ The same site has an **API migration table** (`api.html`, also as `api.json`): every function that existed in a Classic client and is missing, moved or restricted in Forever, with its replacement, plus the deprecation fallbacks, secret-value functions and events. An addon author asked for exactly this list in the Forever beta feedback thread.
149
+
150
+ **"Now works on Forever" feed.** Every week the index is compared with the previous one, and the addons that got better (from "does not load" to "loads", or from errors to clean) go into an RSS feed at `feed.xml` on the same site. Links open that addon's row. To also post them to a Telegram channel, create a bot with @BotFather, add it to the channel, and set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` as repository secrets; without them the step is skipped.
151
+
152
+ ## Where the data comes from
153
+
154
+ Everything in `src/forever_lint/data/api.json` is generated by `tools/build_db.py` from public data, and a weekly workflow rebuilds it when Blizzard patches Forever:
155
+
156
+ - **What exists in Forever and other clients**: the global and namespace API dumped from the running clients by [Ketho/BlizzardInterfaceResources](https://github.com/Ketho/BlizzardInterfaceResources) (branches `forever`, `classic_era`, `classic`, `classic_anniversary`, `live`, dumped with deprecation fallbacks off), plus every name that appeared in its history since 2019, for APIs older addons still call.
157
+ - **Deprecation fallbacks, replacements and secret values**: Blizzard's own UI source as mirrored by [Gethe/wow-ui-source](https://github.com/Gethe/wow-ui-source). That covers the `Blizzard_Deprecated*` addons (which ones load for Forever's game type, and whether their target exists: `CombatLogGetCurrentEventInfo` points to a function addons cannot use), the 11.0 spell API transition guide, and `Blizzard_APIDocumentationGenerated` for `SecretReturns`, `SecretWhen*`, restricted events and structure return types.
158
+ - **TOC rules**: [warcraft.wiki.gg TOC format](https://warcraft.wiki.gg/wiki/TOC_format).
159
+ - A short list of replacements that cannot be derived (`GetMouseFocus` → `GetMouseFoci`, `EasyMenu` → `MenuUtil.CreateContextMenu`, ...) lives in `tools/curated.json`. Each target is checked against Forever when the database is built.
160
+
161
+ Only API names and flags are shipped. No Blizzard code is included.
162
+
163
+ ## Limitations
164
+
165
+ - It is a lexical analysis, not an interpreter. It cannot tell which side of `if isClassic then ... else ... end` runs in Forever, so code in a branch for another client is still reported. Use the guard patterns above, an inline `ignore`, or the TOC's `[AllowLoadGameType]` to keep that code out of Forever.
166
+ - "Probably replaced by" means the replacement was matched by name (`GetAddOnMetadata` → `C_AddOns.GetAddOnMetadata`). Check its arguments and return values; many `C_` functions return a table.
167
+ - Widget methods (`frame:SetBackdrop`) and global constants are not checked yet.
168
+ - Events are read from string literals (`RegisterEvent("PLAYER_DEAD")`) and, when a file registers events from a variable (`for event in pairs(handlers) do frame:RegisterEvent(event) end`), from the keys of its handler tables (`PLAYER_DEAD = function`). Events built at runtime (`"UNIT_" .. kind`) are not followed.
169
+ - Secret values are flagged when the value is used directly, or through a local in the same file, in arithmetic or a comparison. Cached globals (`local UnitHealth = UnitHealth`) count. A check on the same line (`issecretvalue(hp) or`, `E:NotSecretValue(hp) and`) counts as a guard; a check on an earlier line does not. Values passed through tables or other functions are not followed.
170
+
171
+ ## Tested on
172
+
173
+ Questie, Details!, Bartender4, BigWigs and Deadly Boss Mods (November 2026 versions). Questie and Details already ship a `_Camelot.toc`. The few errors left in them are calls to missing APIs in code that Forever's TOC loads.
174
+
175
+ ## Support
176
+
177
+ forever-lint is free and open source. Not affiliated with or endorsed by Blizzard Entertainment. World of Warcraft is a trademark of Blizzard Entertainment, Inc.
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,84 @@
1
+ # forever-lint
2
+
3
+ Revisa addons de World of Warcraft y encuentra lo que se rompe en **WoW Forever**, antes de que lo encuentren tus usuarios.
4
+
5
+ Forever parece Classic, pero corre el cliente moderno: números de versión de Classic sobre la API de retail, con las restricciones a addons de Midnight. El código hecho para Classic Era se rompe de formas fáciles de pasar por alto:
6
+ - `GetSpellInfo` ya no existe;
7
+ - `WOW_PROJECT_ID` no es ni Classic ni retail;
8
+ - la vida puede llegar como valor secreto;
9
+ - y un addon que solo trae `_Vanilla.toc` directamente no carga.
10
+
11
+ [Read in English](README.md)
12
+
13
+ ## Instalar y usar
14
+
15
+ ```bash
16
+ pip install forever-lint # Python 3.10+, sin dependencias
17
+ forever-lint ruta/a/MiAddon # un addon, un repo con varios, o la carpeta AddOns entera
18
+ ```
19
+
20
+ ## Qué revisa
21
+
22
+ | Regla | Severidad | Qué |
23
+ |---|---|---|
24
+ | **FL001** | error | Función, namespace o función de la UI de Blizzard que no existe en Forever, con su reemplazo cuando lo hay. También `hooksecurefunc("Nombre", ...)` sobre una global que no existe, que tira error. |
25
+ | **FL002** | warning | API que solo existe como alias de compatibilidad de Blizzard (mientras el CVar `loadDeprecationFallbacks` está prendido). Blizzard avisa que los va a sacar. |
26
+ | **FL003** | warning | Cuentas o comparaciones con lo que devuelve una función documentada como "secret" (`UnitHealth`, `UnitPower` en ciertos casos...). En un addon eso tira un error de Lua. |
27
+ | **FL004** | warning | Comparaciones con `WOW_PROJECT_ID`, con lo que dan en Forever (18, `WOW_PROJECT_CAMELOT`). |
28
+ | **FL005** | error | `RegisterEvent("EVENTO")` de un evento que Forever no tiene. |
29
+ | **FL006** | warning | Eventos restringidos o con payload secreto, como `COMBAT_LOG_EVENT_UNFILTERED`. |
30
+ | **FL007** | warning | Chequeos de `select(4, GetBuildInfo())`: Forever reporta 16001, así que `>= 100000` da falso. |
31
+ | **FL008** | error | No hay un TOC que Forever cargue (busca `_Camelot`, `_Forever`, `_Mainline` y después el `.toc` sin sufijo). |
32
+ | **FL009** | warning | El `## Interface:` del TOC no incluye 16001, así que el addon aparece como desactualizado. |
33
+ | **FL010** | nota | El TOC excluye a Forever con `AllowLoadGameType`/`ExcludeLoadGameType`. Está bien si es a propósito. |
34
+ | **FL011** | nota | `SendChatMessage` con SAY, YELL o EMOTE: fuera de instancias necesita que el jugador apriete algo (EMOTE desde 1.60.1). Dentro de instancias, un tester de la beta reportó que ya no anda (18/10, sin confirmar). |
35
+ | **FL012** | nota | Las cuentas de prueba no pueden mandar mensajes de addon (1.60.1). Sale una vez por addon. |
36
+ | **FL013** | nota | Eventos que, según un autor de addons en la beta (18/10, sin confirmar), se comportan distinto en Forever: `PLAYER_DEAD` y `MERCHANT_CLOSED`/`AUCTION_HOUSE_CLOSED` saltan dos veces, y la zona justo después de `ZONE_CHANGED_NEW_AREA` puede venir vieja o vacía al viajar. |
37
+
38
+ Revisa solo los archivos que carga el TOC que elige Forever (incluyendo `[AllowLoadGameType]` por archivo y los `<Script>`/`<Include>` de los XML). Así no reporta código de otros clientes.
39
+
40
+ Para no llenarte de falsos positivos:
41
+ - sigue los alcances de Lua, así que un `local GetSpellInfo` no salta;
42
+ - reconoce los polyfills (`GetSpellInfo = GetSpellInfo or function ...`);
43
+ - los guards (`if GetMouseFocus then`) salen como notas;
44
+ - también es nota el fallback que viene después de chequear el reemplazo (`if C_Item and C_Item.GetItemSpell then ... end return GetItemSpell(id)`);
45
+ - las librerías embebidas (`Libs/`) salen resumidas y nunca hacen fallar la corrida.
46
+
47
+ Silenciar: `-- forever-lint: ignore FL001` en la línea, `-- forever-lint: ignore-next-line`, `-- forever-lint: ignore-file`, o `--ignore FL002,FL004`.
48
+
49
+ ## En CI
50
+
51
+ ```yaml
52
+ - uses: JoseArgento/forever-lint@v0
53
+ with:
54
+ path: .
55
+ ```
56
+
57
+ Sube un SARIF a code scanning, así los hallazgos aparecen como anotaciones en el pull request (hace falta `security-events: write`).
58
+
59
+ ## Índice de compatibilidad
60
+
61
+ `index/` arma una web estática: corre forever-lint sobre addons populares con repo público en GitHub (`index/addons.json`) y la publica en GitHub Pages cada semana. Muestra archivo, línea y mensaje de cada hallazgo, nunca el código de los addons. Para sumar un addon, abrí un pull request con su repo en `index/addons.json`.
62
+
63
+ La misma web tiene una **tabla de API movida** (`api.html`, también como `api.json`): cada función que existía en algún Classic y en Forever no está, se movió o está restringida, con su reemplazo, más los alias de compatibilidad, los valores secretos y los eventos.
64
+
65
+ ## De dónde salen los datos
66
+
67
+ `tools/build_db.py` arma `src/forever_lint/data/api.json`, y un workflow semanal lo regenera cuando Blizzard parchea Forever:
68
+ - La API que existe en cada cliente sale de los dumps de [Ketho/BlizzardInterfaceResources](https://github.com/Ketho/BlizzardInterfaceResources), con su historial desde 2019.
69
+ - Los alias de compatibilidad, los reemplazos, los valores secretos y los eventos restringidos salen del código de la UI de Blizzard (espejo [Gethe/wow-ui-source](https://github.com/Gethe/wow-ui-source)), incluida `Blizzard_APIDocumentationGenerated`.
70
+
71
+ Solo se publican nombres y flags; nada de código de Blizzard.
72
+
73
+ ## Feed "ya anda en Forever"
74
+
75
+ Cada semana el índice se compara con el anterior, y los addons que mejoraron (de "no carga" a "carga", o de errores a limpio) salen en un feed RSS, `feed.xml`, en el mismo sitio. Para mandarlos también a un canal de Telegram: creá un bot con @BotFather, sumalo al canal y cargá `TELEGRAM_BOT_TOKEN` y `TELEGRAM_CHAT_ID` como secretos del repo. Sin ellos ese paso no corre.
76
+
77
+ ## Límites
78
+
79
+ - Es análisis léxico: no sabe qué rama de un `if esClassic then ... else ... end` corre en Forever.
80
+ - "Probably replaced by" es un reemplazo encontrado por nombre. Revisá argumentos y retornos (muchas `C_` devuelven una tabla).
81
+ - Todavía no revisa métodos de widgets ni constantes globales.
82
+ - Los eventos los lee de textos fijos (`RegisterEvent("PLAYER_DEAD")`) y, si un archivo los registra desde una variable (`for event in pairs(handlers) do frame:RegisterEvent(event) end`), de las claves de sus tablas de manejadores (`PLAYER_DEAD = function`). Los que se arman al correr (`"UNIT_" .. tipo`) no los sigue.
83
+
84
+ Sin relación con Blizzard Entertainment. World of Warcraft es marca registrada de Blizzard Entertainment, Inc. Licencia MIT.
@@ -0,0 +1,161 @@
1
+ # forever-lint
2
+
3
+ Checks World of Warcraft addons for problems on **WoW Forever**, before your users find them.
4
+
5
+ Forever looks like Classic but runs the modern client: Classic-era version numbers on top of the retail API, with Midnight's addon restrictions. Code written for Classic Era breaks in ways that are easy to miss: `GetSpellInfo` is gone, `WOW_PROJECT_ID` is neither Classic nor retail, health values can be secret, and an addon with only a `_Vanilla.toc` does not load at all.
6
+
7
+ ```
8
+ $ forever-lint MyAddon
9
+ forever-lint 0.1.0 · WoW Forever 1.60.1 (70532), interface 16001
10
+
11
+ MyAddon (MyAddon, MyAddon.toc, 2 Lua files)
12
+ warning FL009 MyAddon.toc declares Interface 11509, 120105 but Forever is 16001: the addon shows as out of date ...
13
+
14
+ MyAddon/Core.lua
15
+ 2:22 error FL001 `GetSpellInfo` does not exist in Forever. Probably replaced by `C_Spell.GetSpellInfo`. Note: `C_Spell.GetSpellInfo` returns a SpellInfo table, not separate values.
16
+ 4:19 warning FL004 In Forever `WOW_PROJECT_ID` is 18 (`WOW_PROJECT_CAMELOT`), so `WOW_PROJECT_ID == WOW_PROJECT_CLASSIC` is false. ...
17
+ 10:10 warning FL003 `hp` holds the result of `UnitHealth`, which can return a secret value in Forever (always). ...
18
+ 14:17 warning FL006 Event `COMBAT_LOG_EVENT_UNFILTERED`: the combat log is restricted for addons in Forever ...
19
+ 18:16 error FL001 `GetItemInfo` does not exist in Forever. Use `C_Item.GetItemInfo`.
20
+
21
+ 8 errors, 6 warnings, 1 note in 1 addon(s)
22
+ ```
23
+
24
+ [Leer en castellano](README.es.md)
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install forever-lint # Python 3.10+, no dependencies
30
+ forever-lint path/to/MyAddon # one addon, a repo with several, or your whole AddOns folder
31
+ ```
32
+
33
+ ## What it checks
34
+
35
+ | Rule | Severity | What |
36
+ |---|---|---|
37
+ | <a id="fl001-removed-api"></a>**FL001** removed-api | error | A function, namespace or Blizzard UI function that does not exist in Forever, with its replacement when there is one. Also `hooksecurefunc("Name", ...)` on a missing global, which raises an error. |
38
+ | <a id="fl002-deprecated-api"></a>**FL002** deprecated-api | warning | API that only exists as one of Blizzard's deprecation fallbacks (loaded while the `loadDeprecationFallbacks` CVar is on). Blizzard says these will be removed. |
39
+ | <a id="fl003-secret-value"></a>**FL003** secret-value | warning | Arithmetic or comparisons on the result of a function documented to return secret values (`UnitHealth`, `UnitPower` when restricted, `frame:GetFrameStrata()` since 1.60.1, ...). In addon code that raises a Lua error. |
40
+ | <a id="fl004-project-id"></a>**FL004** project-id | warning | `WOW_PROJECT_ID` comparisons, with what they evaluate to in Forever (18, `WOW_PROJECT_CAMELOT`). Lines that already mention `WOW_PROJECT_CAMELOT` are notes. |
41
+ | <a id="fl005-removed-event"></a>**FL005** removed-event | error | `RegisterEvent("EVENT")` for an event Forever does not have. |
42
+ | <a id="fl006-restricted-event"></a>**FL006** restricted-event | warning | Events with restrictions or secret payloads, such as `COMBAT_LOG_EVENT_UNFILTERED`. |
43
+ | <a id="fl007-build-version"></a>**FL007** build-version | warning | `select(4, GetBuildInfo())` checks: Forever reports 16001, so "modern client" tests like `>= 100000` are false. |
44
+ | <a id="fl008-no-forever-toc"></a>**FL008** no-forever-toc | error | No TOC that Forever loads. It looks for `_Camelot`, `_Forever`, `_Mainline`, then the plain `.toc`. |
45
+ | <a id="fl009-toc-interface"></a>**FL009** toc-interface | warning | The TOC's `## Interface:` does not list 16001, so the addon shows as out of date. |
46
+ | <a id="fl010-toc-game-type"></a>**FL010** toc-game-type | note | The TOC excludes Forever with `AllowLoadGameType`/`ExcludeLoadGameType`. Fine if on purpose. |
47
+ | <a id="fl011-chat-hardware-event"></a>**FL011** chat-hardware-event | note | `SendChatMessage` with SAY, YELL or EMOTE: outside instances it needs a key press or click (EMOTE since 1.60.1). Inside instances a beta tester reported it no longer works at all (18/10, not confirmed). |
48
+ | <a id="fl012-trial-addon-messages"></a>**FL012** trial-addon-messages | note | Trial accounts cannot send addon messages (1.60.1). Reported once per addon. |
49
+ | <a id="fl013-beta-event-quirk"></a>**FL013** beta-event-quirk | note | Events an addon author reported behave differently in Forever's beta (18/10, not confirmed): `PLAYER_DEAD` and the `MERCHANT_CLOSED`/`AUCTION_HOUSE_CLOSED` pair fire twice, and the zone text right after `ZONE_CHANGED_NEW_AREA` can be stale or empty after travelling. |
50
+
51
+ It follows the TOC Forever would pick, including `[AllowLoadGameType ...]` per file and the `<Script>`/`<Include>` files of XML, so code for other clients is not reported. `--all-files` checks every `.lua` file instead.
52
+
53
+ ### Fewer false positives
54
+
55
+ The analysis tracks Lua scopes, so a local or parameter called `GetSpellInfo` is not reported. It also recognises the usual multi-client patterns:
56
+
57
+ - **Polyfills**: if the addon assigns the global itself (`GetSpellInfo = GetSpellInfo or function ...`), uses anywhere in the addon are fine.
58
+ - **Guards**: `if GetMouseFocus then`, `GetMouseFocus and ...`, `C_X and C_X.Y or Old` become notes, because the fallback branch is the one that runs in Forever.
59
+ - **Checked replacement**: `if C_Item and C_Item.GetItemSpell then return C_Item.GetItemSpell(id) end return GetItemSpell(id)` is a note. Forever has the new function, so the old line does not run there.
60
+ - **Embedded libraries** (`Libs/`, `lib/`, ...) are summarised, never fail the run, and are listed with `--libs`.
61
+
62
+ ### Silencing a finding
63
+
64
+ ```lua
65
+ local x = GetMouseFocus() -- forever-lint: ignore FL001
66
+ -- forever-lint: ignore-next-line
67
+ OldThing()
68
+ -- forever-lint: ignore-file
69
+ ```
70
+
71
+ Or skip whole rules with `--ignore FL002,FL004`.
72
+
73
+ ## With luacheck or pre-commit
74
+
75
+ `forever-lint --export-luacheck > .luacheckrc.forever` prints a `read_globals` block with every function Forever has (deprecation fallbacks left out), to paste into an existing `.luacheckrc`.
76
+
77
+ For [pre-commit](https://pre-commit.com):
78
+
79
+ ```yaml
80
+ repos:
81
+ - repo: https://github.com/JoseArgento/forever-lint
82
+ rev: v0.1.0
83
+ hooks:
84
+ - id: forever-lint
85
+ ```
86
+
87
+ ## Options
88
+
89
+ ```
90
+ forever-lint PATH... [--format text|json|sarif] [-o FILE] [--fail-on error|warning|note|never]
91
+ [--ignore RULES] [--libs] [--all-files] [--no-color]
92
+ forever-lint --explain GetSpellInfo # what Forever has for a name
93
+ forever-lint --list-rules
94
+ ```
95
+
96
+ The exit code is 1 when there is a finding at the `--fail-on` severity (default `error`) outside libraries.
97
+
98
+ ## In CI (GitHub Actions)
99
+
100
+ Findings show up as code scanning annotations on the pull request:
101
+
102
+ ```yaml
103
+ jobs:
104
+ forever:
105
+ runs-on: ubuntu-latest
106
+ permissions:
107
+ contents: read
108
+ security-events: write
109
+ steps:
110
+ - uses: actions/checkout@v4
111
+ - uses: JoseArgento/forever-lint@v0
112
+ with:
113
+ path: .
114
+ fail-on: error
115
+ ```
116
+
117
+ Without the action: `pip install forever-lint && forever-lint . --format sarif -o forever-lint.sarif`, then upload the file with `github/codeql-action/upload-sarif`.
118
+
119
+ ## Compatibility index
120
+
121
+ `index/` builds a static site that runs forever-lint over popular addons with public GitHub repositories (`index/addons.json`) and publishes the results with GitHub Pages every week. It shows file, line and message for every finding, never the addons' code. To add an addon, open a pull request with its repository in `index/addons.json`.
122
+
123
+ ```bash
124
+ python index/build_index.py --cache .index-cache --out site # then open site/index.html
125
+ python index/build_api.py --out site # site/api.html: every Classic API that moved
126
+ ```
127
+
128
+ The same site has an **API migration table** (`api.html`, also as `api.json`): every function that existed in a Classic client and is missing, moved or restricted in Forever, with its replacement, plus the deprecation fallbacks, secret-value functions and events. An addon author asked for exactly this list in the Forever beta feedback thread.
129
+
130
+ **"Now works on Forever" feed.** Every week the index is compared with the previous one, and the addons that got better (from "does not load" to "loads", or from errors to clean) go into an RSS feed at `feed.xml` on the same site. Links open that addon's row. To also post them to a Telegram channel, create a bot with @BotFather, add it to the channel, and set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` as repository secrets; without them the step is skipped.
131
+
132
+ ## Where the data comes from
133
+
134
+ Everything in `src/forever_lint/data/api.json` is generated by `tools/build_db.py` from public data, and a weekly workflow rebuilds it when Blizzard patches Forever:
135
+
136
+ - **What exists in Forever and other clients**: the global and namespace API dumped from the running clients by [Ketho/BlizzardInterfaceResources](https://github.com/Ketho/BlizzardInterfaceResources) (branches `forever`, `classic_era`, `classic`, `classic_anniversary`, `live`, dumped with deprecation fallbacks off), plus every name that appeared in its history since 2019, for APIs older addons still call.
137
+ - **Deprecation fallbacks, replacements and secret values**: Blizzard's own UI source as mirrored by [Gethe/wow-ui-source](https://github.com/Gethe/wow-ui-source). That covers the `Blizzard_Deprecated*` addons (which ones load for Forever's game type, and whether their target exists: `CombatLogGetCurrentEventInfo` points to a function addons cannot use), the 11.0 spell API transition guide, and `Blizzard_APIDocumentationGenerated` for `SecretReturns`, `SecretWhen*`, restricted events and structure return types.
138
+ - **TOC rules**: [warcraft.wiki.gg TOC format](https://warcraft.wiki.gg/wiki/TOC_format).
139
+ - A short list of replacements that cannot be derived (`GetMouseFocus` → `GetMouseFoci`, `EasyMenu` → `MenuUtil.CreateContextMenu`, ...) lives in `tools/curated.json`. Each target is checked against Forever when the database is built.
140
+
141
+ Only API names and flags are shipped. No Blizzard code is included.
142
+
143
+ ## Limitations
144
+
145
+ - It is a lexical analysis, not an interpreter. It cannot tell which side of `if isClassic then ... else ... end` runs in Forever, so code in a branch for another client is still reported. Use the guard patterns above, an inline `ignore`, or the TOC's `[AllowLoadGameType]` to keep that code out of Forever.
146
+ - "Probably replaced by" means the replacement was matched by name (`GetAddOnMetadata` → `C_AddOns.GetAddOnMetadata`). Check its arguments and return values; many `C_` functions return a table.
147
+ - Widget methods (`frame:SetBackdrop`) and global constants are not checked yet.
148
+ - Events are read from string literals (`RegisterEvent("PLAYER_DEAD")`) and, when a file registers events from a variable (`for event in pairs(handlers) do frame:RegisterEvent(event) end`), from the keys of its handler tables (`PLAYER_DEAD = function`). Events built at runtime (`"UNIT_" .. kind`) are not followed.
149
+ - Secret values are flagged when the value is used directly, or through a local in the same file, in arithmetic or a comparison. Cached globals (`local UnitHealth = UnitHealth`) count. A check on the same line (`issecretvalue(hp) or`, `E:NotSecretValue(hp) and`) counts as a guard; a check on an earlier line does not. Values passed through tables or other functions are not followed.
150
+
151
+ ## Tested on
152
+
153
+ Questie, Details!, Bartender4, BigWigs and Deadly Boss Mods (November 2026 versions). Questie and Details already ship a `_Camelot.toc`. The few errors left in them are calls to missing APIs in code that Forever's TOC loads.
154
+
155
+ ## Support
156
+
157
+ forever-lint is free and open source. Not affiliated with or endorsed by Blizzard Entertainment. World of Warcraft is a trademark of Blizzard Entertainment, Inc.
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,38 @@
1
+ name: forever-lint
2
+ description: Check World of Warcraft addons for problems on WoW Forever and upload the results to GitHub code scanning.
3
+ branding:
4
+ icon: check-circle
5
+ color: orange
6
+ inputs:
7
+ path:
8
+ description: Addon folder(s) to check, space separated
9
+ default: "."
10
+ fail-on:
11
+ description: "Fail the step at this severity or worse: error, warning, note or never"
12
+ default: error
13
+ sarif:
14
+ description: Upload a SARIF report to code scanning (needs security-events write permission)
15
+ default: "true"
16
+ version:
17
+ description: forever-lint version to install (default the latest)
18
+ default: ""
19
+ runs:
20
+ using: composite
21
+ steps:
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.12"
25
+ - shell: bash
26
+ run: pip install "forever-lint${{ inputs.version && format('=={0}', inputs.version) || '' }}"
27
+ - shell: bash
28
+ run: forever-lint ${{ inputs.path }} --no-color --fail-on never
29
+ - if: inputs.sarif == 'true'
30
+ shell: bash
31
+ run: forever-lint ${{ inputs.path }} --format sarif --fail-on never -o forever-lint.sarif
32
+ - if: inputs.sarif == 'true'
33
+ uses: github/codeql-action/upload-sarif@v3
34
+ with:
35
+ sarif_file: forever-lint.sarif
36
+ category: forever-lint
37
+ - shell: bash
38
+ run: forever-lint ${{ inputs.path }} --no-color --fail-on ${{ inputs.fail-on }} > /dev/null
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "forever-lint"
7
+ version = "0.1.0"
8
+ description = "Checks World of Warcraft addons for problems on WoW Forever: removed API, secret values, project ID checks, events and TOC files."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "José Argento" }]
13
+ requires-python = ">=3.10"
14
+ dependencies = []
15
+ keywords = ["world-of-warcraft", "wow", "addon", "lua", "lint", "static-analysis", "sarif"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Lua",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Software Development :: Quality Assurance",
23
+ "Topic :: Games/Entertainment",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/JoseArgento/forever-lint"
28
+ Issues = "https://github.com/JoseArgento/forever-lint/issues"
29
+
30
+ [project.scripts]
31
+ forever-lint = "forever_lint.cli:main"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/forever_lint"]
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ include = ["src", "tools", "tests", "README.md", "README.es.md", "LICENSE", "CHANGELOG.md", "action.yml"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ pythonpath = ["src"]
42
+
43
+ [tool.ruff]
44
+ line-length = 140
45
+ target-version = "py310"
46
+
47
+ [tool.ruff.lint]
48
+ select = ["E", "F", "W", "I", "B", "UP"]
49
+ ignore = ["E501"]
@@ -0,0 +1,3 @@
1
+ """forever-lint: checks World of Warcraft addons for problems on WoW Forever."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())