From a25f8c8611575057dfb7ceb1c368371b80a368be Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:25:24 +0000 Subject: [PATCH 1/3] Fix lua lint warnings reported by emmylua The largest source was compat.lua returning the setmetatable call directly, which made emmylua infer the module type from the metatable rather than the LibCompat class, hiding every field from consumers. The remaining fixes are nil guards on gui element and character accesses, type annotations for research_sync's script data and player_auth's rcon arguments, casts where the api's read/write union types are wider than the values at runtime, and scoped diagnostic disables for the vendored json.lua and the version fallback code that touches apis removed from the current typedefs. Co-Authored-By: Claude Fable 5 --- packages/host/lua/export/control.lua | 3 +++ packages/host/modules/clusterio/compat.lua | 7 +++++-- packages/host/modules/clusterio/json.lua | 3 +++ .../module/restore_position.lua | 5 +++-- plugins/inventory_sync/module/serialize.lua | 19 ++++++++++--------- plugins/player_auth/module/auth.lua | 13 ++++++++----- plugins/research_sync/module/sync.lua | 11 ++++++++++- plugins/statistics_exporter/module/export.lua | 1 + 8 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/host/lua/export/control.lua b/packages/host/lua/export/control.lua index ae895a8c..402dc1b7 100644 --- a/packages/host/lua/export/control.lua +++ b/packages/host/lua/export/control.lua @@ -1,3 +1,6 @@ +--- Undefined fields are expected, the typedefs only cover the latest version +--- @diagnostic disable: undefined-field + local function send_json(channel, data) data = helpers and helpers.table_to_json(data) or game.table_to_json(data) print("\f$ipc:" .. channel .. "?j" .. data) diff --git a/packages/host/modules/clusterio/compat.lua b/packages/host/modules/clusterio/compat.lua index cafee324..4cd2d7c1 100644 --- a/packages/host/modules/clusterio/compat.lua +++ b/packages/host/modules/clusterio/compat.lua @@ -10,7 +10,8 @@ Implementation Guidelines local lib_json = require("json") ---- @diagnostic disable: deprecated +--- Undefined fields are expected, the typedefs only cover the latest version +--- @diagnostic disable: deprecated, undefined-field --- @class LibCompat --- @field script_data table @@ -238,4 +239,6 @@ set_script_data() compat.on_init = set_script_data --- @package compat.on_load = set_script_data --- @package -return setmetatable(compat, compat_mt) +-- Not returned directly because that would infer the metatable type rather than LibCompat +setmetatable(compat, compat_mt) +return compat diff --git a/packages/host/modules/clusterio/json.lua b/packages/host/modules/clusterio/json.lua index be01e897..01a01382 100644 --- a/packages/host/modules/clusterio/json.lua +++ b/packages/host/modules/clusterio/json.lua @@ -22,6 +22,9 @@ -- SOFTWARE. -- +--- Vendored file, the forward declared recursion is not worth annotating +--- @diagnostic disable: need-check-nil + local json = { _version = "0.1.2" } ------------------------------------------------------------------------------- diff --git a/plugins/inventory_sync/module/restore_position.lua b/plugins/inventory_sync/module/restore_position.lua index f2513133..91da5ea7 100644 --- a/plugins/inventory_sync/module/restore_position.lua +++ b/plugins/inventory_sync/module/restore_position.lua @@ -21,9 +21,10 @@ return function(player, record) player.driving = true -- Teleport to safe location if unable to enter vehicle - if not player.driving and player.controller_type == defines.controllers.character then + local character = player.character + if not player.driving and character and player.controller_type == defines.controllers.character then local safe_position = record.vehicle.surface.find_non_colliding_position( - player.character.name, player.position, 32, 1/8 + character.name, player.position, 32, 1/8 ) if safe_position then player.teleport(safe_position, player.surface) diff --git a/plugins/inventory_sync/module/serialize.lua b/plugins/inventory_sync/module/serialize.lua index a46f6472..9c0aa509 100644 --- a/plugins/inventory_sync/module/serialize.lua +++ b/plugins/inventory_sync/module/serialize.lua @@ -207,14 +207,14 @@ end --- @return QuickBarSlotEncoded local function serialize_filter(filter) if filter.quality == "normal" and filter.comparator == "=" then - return filter.name + return filter.name --[[@as string]] end return { t = "f", - n = filter.name, - q = filter.quality, - c = filter.comparator, + n = filter.name --[[@as string]], + q = filter.quality --[[@as string]], + c = filter.comparator --[[@as string]], } end @@ -234,7 +234,7 @@ function serialize.serialize_quick_bar_slot(slot) if v2_0_quick_bar_api then -- 2.0 gives us an item filter which supports quality - --- @cast slot ItemFilter + --- @cast slot -QuickBarSlot, -LuaItemPrototype return serialize_filter(slot) end @@ -267,7 +267,7 @@ function serialize.deserialize_quick_bar_slot(entry) quality = entry.q, comparator = entry.c, }, - } + } --[[@as QuickBarSlot]] end print("Warning: Unsupported serialized quick bar slot type '" .. tostring(entry.t) .. "'") @@ -292,7 +292,7 @@ function serialize.deserialize_quick_bar_slot(entry) -- Return a string (member of ItemPrototypeIdentification) -- Pre 2.0 this was the only method of encoding used - return entry + return entry --[[@as string]] end --- @param player LuaPlayer @@ -709,7 +709,8 @@ local function ensure_character(player) local surface = player.surface if v2_space_platform and surface.platform then local planet_surface = find_planet_surface(surface.platform) - player.teleport(player.force.get_spawn_position(planet_surface), planet_surface) + local force = player.force --[[@as LuaForce]] + player.teleport(force.get_spawn_position(planet_surface), planet_surface) end -- Create and return the character @@ -721,7 +722,7 @@ local function ensure_character(player) player.surface.name, tostring(player.connected), tostring(player.driving) )) end - return player.character + return assert(player.character) end --- @class FailedDeserializationPlayerData diff --git a/plugins/player_auth/module/auth.lua b/plugins/player_auth/module/auth.lua index ad195f89..7bbdb9da 100644 --- a/plugins/player_auth/module/auth.lua +++ b/plugins/player_auth/module/auth.lua @@ -132,6 +132,7 @@ function auth.add_commands() end if event.parameter then + --- @type string[] local args = {} for w in string.gmatch(event.parameter, "[^ ]+") do args[#args + 1] = w @@ -149,7 +150,7 @@ function auth.add_commands() end local player_name, url, code = table.unpack(args) - local player = game.players[player_name] + local player = game.get_player(player_name) if not player then rcon.print("Player " .. player_name .. " does not exist") return @@ -159,7 +160,7 @@ function auth.add_commands() elseif command == "code_set" then local player_name = args[1] - local player = game.players[player_name] + local player = game.get_player(player_name) if not player then rcon.print("Player " .. player_name .. " does not exist") return @@ -171,7 +172,7 @@ function auth.add_commands() elseif command == "error" then local player_name = table.remove(args, 1) - local player = game.players[player_name] + local player = game.get_player(player_name) if not player then rcon.print("Player " .. player_name .. " does not exist") return @@ -192,10 +193,12 @@ auth.events[defines.events.on_gui_click] = function(event) end if event.element.name == "player_auth_dialog_close_button" then - event.element.parent.parent.destroy() + local parent = assert(event.element.parent) + assert(parent.parent).destroy() elseif event.element.name == "player_auth_verify_code_button" then - local verify_code = event.element.parent.player_auth_verify_code_input.text + local parent = assert(event.element.parent) + local verify_code = assert(parent.player_auth_verify_code_input).text local player_name = game.players[event.player_index].name clusterio_api.send_json("player_auth", { type = "set_verify_code", diff --git a/plugins/research_sync/module/sync.lua b/plugins/research_sync/module/sync.lua index 7db1f20d..701f4f7e 100644 --- a/plugins/research_sync/module/sync.lua +++ b/plugins/research_sync/module/sync.lua @@ -30,8 +30,17 @@ local function set_technology_progress(tech, progress) end end +--- @class ResearchSyncTechnology +--- @field level uint +--- @field researched boolean? +--- @field progress double? + +--- @class ResearchSyncData +--- @field technologies table +--- @field ignore_research_finished boolean + --- @param no_early_return boolean? ---- @return table +--- @return ResearchSyncData local function get_script_data(no_early_return) local research_sync = compat.script_data.research_sync if research_sync and not no_early_return then diff --git a/plugins/statistics_exporter/module/export.lua b/plugins/statistics_exporter/module/export.lua index 33626000..6a4bb75b 100644 --- a/plugins/statistics_exporter/module/export.lua +++ b/plugins/statistics_exporter/module/export.lua @@ -32,6 +32,7 @@ function statistics_exporter.export() for _, force in pairs(game.forces) do local flow_statistics = {} for _, statName in pairs(statistics) do + --- @type LuaFlowStatistics local stat = v2_stats and force["get_" .. statName](surface.index) or force[statName] flow_statistics[statName] = { input = stat.input_counts, From 4caf585048a50e755663aaefafd9330e4b075c02 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:25:24 +0000 Subject: [PATCH 2/3] Add emmylua lua lint CI check Same setup as established in ExpCluster, emmylua_check with factorio typedefs generated by fmtk luals-addon. The moduleMap resolves modules/clusterio and modules/ requires to their source under packages/host and plugins, and the mod scaffolding under packages/host/lua to bare sibling requires. Co-Authored-By: Claude Fable 5 --- .emmyrc.json | 48 ++++++++++++++++++++ .github/workflows/lua-lint.yml | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 .emmyrc.json create mode 100644 .github/workflows/lua-lint.yml diff --git a/.emmyrc.json b/.emmyrc.json new file mode 100644 index 00000000..2d308c82 --- /dev/null +++ b/.emmyrc.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json", + "runtime": { + "version": "Lua5.2", + "requirePattern": [ "?", "?.lua" ] + }, + "workspace": { + "$comment-moduleMap": "Rewrites each file's own module path rather than the require string", + "$comment-library": "The factorio typedefs are provided by a second config, generated with fmtk luals-addon in ci", + "$comment-templates": "The create templates contain __plugin_name__ placeholders and are not valid lua", + "ignoreGlobs": [ + "**/node_modules/**", + "**/dist/**", + "database/**", + "docs/**", + "external_plugins/**", + "factorio/**", + "instances/**", + "logs/**", + "mods/**", + "modules/**", + "temp/**", + "packages/create/templates/**" + ], + "moduleMap": [ + { "pattern": "^packages\\.host\\.modules\\.(.+)$", "replace": "modules.$1" }, + { "pattern": "^plugins\\.([^.]+)\\.module\\.(.+)$", "replace": "modules.$1.$2" }, + { "pattern": "^packages\\.host\\.lua\\.export\\.(.+)$", "replace": "$1" }, + { "pattern": "^packages\\.host\\.lua\\.clusterio_lib\\.(.+)$", "replace": "$1" } + ] + }, + "doc": { + "privateName": [ "__*" ] + }, + "$comment-disable": "Same set as ExpCluster, these are editor level hints rather than ci checks", + "$comment-unnecessary-if": "Disabled by the config fmtk generates for itself, the api union types make it unreliable", + "diagnostics": { + "globals": [ "__DebugAdapter", "__Profiler" ], + "disable": [ + "assign-type-mismatch", + "param-type-mismatch", + "preferred-local-alias", + "unnecessary-assert", + "unnecessary-if", + "unused" + ] + } +} diff --git a/.github/workflows/lua-lint.yml b/.github/workflows/lua-lint.yml new file mode 100644 index 00000000..35b51ad2 --- /dev/null +++ b/.github/workflows/lua-lint.yml @@ -0,0 +1,83 @@ +name: Lua Lint + +on: + push: + pull_request: + +env: + EMMYLUA_CHECK_VERSION: "0.24.0" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Generate Factorio typedefs + run: | + npm install --prefix "$RUNNER_TEMP/fmtk" factoriomod-debug + "$RUNNER_TEMP/fmtk/node_modules/.bin/fmtk" luals-addon "$RUNNER_TEMP" + - name: Install emmylua_check + run: | + curl -fsSL "https://github.com/EmmyLuaLs/emmylua-analyzer-rust/releases/download/${EMMYLUA_CHECK_VERSION}/emmylua_check-linux-x64.tar.gz" \ + | tar -xz -C "$RUNNER_TEMP" + - name: Run Lint Report + shell: bash + run: | + # The typedefs are generated per run, so their path is passed via a second config + jq -n --arg lib "$RUNNER_TEMP/factorio/library" \ + '{ workspace: { library: [ $lib ] } }' \ + > "$RUNNER_TEMP/ci.emmyrc.json" + + # Exits non zero on error severity, so the gate below is used instead + "$RUNNER_TEMP/emmylua_check" . --config ".emmyrc.json,$RUNNER_TEMP/ci.emmyrc.json" \ + --output-format json --output check.json || true + + # Credit to https://github.com/Krealle/luals-check-action/blob/main/action.yml + # Although some minor fixes were needed + + # Format and print the messages (would be nice to format as a table, but that's too complex for jq) + jq -r --arg root "$PWD/" \ + '.[] | (.file | ltrimstr($root)) as $file | .diagnostics[] | + { + file: $file, + code: .code, + line: .range.start.line, + message: (.message | split("\n") + | map(if startswith("- ") then .[2:] else . end) + | join("\n")), + severity: (if .severity <= 1 then "ERR" else "WARN" end) + } | + "**[\(.severity)] \(.code):** \(.file)#L\(.line + 1)
\(.message)"' \ + check.json > "$GITHUB_STEP_SUMMARY" + + echo "" + + # Github Annotations (limited to 10) + jq -r --arg root "$PWD/" \ + '.[] | (.file | ltrimstr($root)) as $file | .diagnostics[] | + { + file: $file, + title: .code, + line: .range.start.line, + endLine: .range.end.line, + col: .range.start.character, + endColumn: .range.end.character, + message: (.message | gsub("\n"; "%0A"; "m")), + level: (if .severity <= 1 then "error" else "warning" end) + } | + "::\(.level) file=\(.file),line=\(.line + 1),endLine=\(.endLine + 1),col=\(.col + 1),endColumn=\(.endColumn + 1),title=\(.title)::\(.message) (\(.title))"' \ + check.json + + # An entry is emitted for every file checked, including clean ones, so + # the gate counts diagnostics and asserts the workspace was actually read + if [[ $(jq 'length' check.json) -lt 20 ]] ; then + echo "::error::Only $(jq 'length' check.json) files were checked, expected the whole workspace" + exit 1 + elif [[ $(jq '[.[].diagnostics[]] | length' check.json) -gt 0 ]] ; then + exit 1 + else + echo "✅ All checks succeeded!" | tee "$GITHUB_STEP_SUMMARY" + fi From 4d75f8d402e870fa1f2e30e5f50180fd3b8d1911 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:15:56 +0100 Subject: [PATCH 3/3] Add luarc to git ignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 07182d0f..e6710c96 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ node_modules /config-*.json.lock pnpm-lock.yaml mod_cache.json +.luarc.json /static/ saves /temp/