Merge pull request #979 from bbassie/feature/emmylua-lint

Add emmylua lint CI for module Lua
This commit is contained in:
Cooldude2606 authored and GitHub committed 2026-08-31 00:25:06 +03:00
commit 1915250529
11 files changed
+175 -19

No files matched your search

+48
View File
@@ -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"
]
}
}
+83
View File
@@ -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)<br><code>\(.message)</code>"' \
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
+1
View File
@@ -38,6 +38,7 @@ node_modules
/config-*.json.lock
pnpm-lock.yaml
mod_cache.json
.luarc.json
/static/
saves
/temp/
+3
View File
@@ -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)
+5 -2
View File
@@ -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
+3
View File
@@ -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" }
-------------------------------------------------------------------------------
@@ -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)
+10 -9
View File
@@ -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
+8 -5
View File
@@ -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",
+10 -1
View File
@@ -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<string, ResearchSyncTechnology>
--- @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
@@ -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,