Fix LuaLS findings in the roles module

Checked with the same LuaLS version and factorio library CI uses. The module now
reports no findings; the 279 which remain are all pre-existing and main is
already failing on them.

Most were annotations rather than behaviour. Two are worth noting:

- The role prototype is now its own class which the role inherits, since
  defining methods on a table annotated as the role counted as injecting fields
  into it.
- The role change message is built from a literal locale key per branch. Building
  the key by concatenation gives a plain string where a localised string is
  wanted, which is the same finding the legacy module reports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
bbassie
2026-07-31 09:39:04 +00:00
co-authored by Claude Opus 5
parent bbd3b7c1f2
commit fcfa174794
+34 -21
View File
@@ -17,7 +17,6 @@ local Async = require("modules/exp_util/async")
--- @class ExpRoles --- @class ExpRoles
local ExpRoles = { local ExpRoles = {
_prototype = {},
config = { config = {
--- Role names in order, a lower index is a more privileged role --- Role names in order, a lower index is a more privileged role
order = {}, --- @type string[] order = {}, --- @type string[]
@@ -25,8 +24,8 @@ local ExpRoles = {
roles = {}, --- @type table<string, ExpRoles.Role> roles = {}, --- @type table<string, ExpRoles.Role>
--- Role names held by each player, includes local assignments --- Role names held by each player, includes local assignments
players = {}, --- @type table<string, string[]> players = {}, --- @type table<string, string[]>
--- Callbacks run when a flag is gained or lost --- Async handles run when a flag is gained or lost
flags = {}, --- @type table<string, fun(player: LuaPlayer, state: boolean)> flags = {}, --- @type table<string, Async.AsyncFunction>
}, },
events = { events = {
on_role_assigned = script.generate_event_name(), on_role_assigned = script.generate_event_name(),
@@ -34,7 +33,13 @@ local ExpRoles = {
}, },
} }
--- @class ExpRoles.Role --- Methods shared by every role, kept apart from the fields so that defining
--- them does not count as injecting fields into the role itself
--- @class ExpRoles.RolePrototype
local Role = {}
ExpRoles._prototype = Role
--- @class ExpRoles.Role : ExpRoles.RolePrototype
--- @field id number Clusterio role id --- @field id number Clusterio role id
--- @field name string --- @field name string
--- @field short_hand string --- @field short_hand string
@@ -150,6 +155,7 @@ local function decode_role(record)
if flag then flags[flag] = true end if flag then flags[flag] = true end
end end
--- @type ExpRoles.Role
return setmetatable({ return setmetatable({
id = record.id, id = record.id,
name = record.name, name = record.name,
@@ -228,8 +234,9 @@ end
--- @return ExpRoles.Role? --- @return ExpRoles.Role?
function ExpRoles.get_role_from_any(any) function ExpRoles.get_role_from_any(any)
local t_any = type(any) local t_any = type(any)
if t_any == "number" or tonumber(any) then local as_number = tonumber(any)
return ExpRoles.get_role_by_order(tonumber(any)) if as_number then
return ExpRoles.get_role_by_order(as_number)
elseif t_any == "string" then elseif t_any == "string" then
return ExpRoles.get_role_by_name(any) return ExpRoles.get_role_by_name(any)
elseif t_any == "table" then elseif t_any == "table" then
@@ -381,24 +388,27 @@ end
]] ]]
--- Check if this role allows an action --- Check if this role allows an action
--- @param self ExpRoles.Role
--- @param action string --- @param action string
--- @return boolean --- @return boolean
function ExpRoles._prototype:is_allowed(action) function Role:is_allowed(action)
local allowed = self.allowed_actions local allowed = self.allowed_actions
return allowed["core.admin"] or allowed[permission_from_action(action)] or false return allowed["core.admin"] or allowed[permission_from_action(action)] or false
end end
--- Check if this role sets a flag --- Check if this role sets a flag
--- @param self ExpRoles.Role
--- @param name string --- @param name string
--- @return boolean --- @return boolean
function ExpRoles._prototype:has_flag(name) function Role:has_flag(name)
return self.allowed_actions[permission_from_flag(name)] or false return self.allowed_actions[permission_from_flag(name)] or false
end end
--- Get the players who hold this role --- Get the players who hold this role
--- @param self ExpRoles.Role
--- @param online boolean? When given, filter by connected state --- @param online boolean? When given, filter by connected state
--- @return LuaPlayer[] --- @return LuaPlayer[]
function ExpRoles._prototype:get_players(online) function Role:get_players(online)
local players = {} local players = {}
for player_name, role_names in pairs(ExpRoles.config.players) do for player_name, role_names in pairs(ExpRoles.config.players) do
for _, role_name in pairs(role_names) do for _, role_name in pairs(role_names) do
@@ -416,9 +426,10 @@ function ExpRoles._prototype:get_players(online)
end end
--- Print a message to every online player who holds this role --- Print a message to every online player who holds this role
--- @param self ExpRoles.Role
--- @param message LocalisedString --- @param message LocalisedString
--- @return number # Number of players the message was sent to --- @return number # Number of players the message was sent to
function ExpRoles._prototype:print(message) function Role:print(message)
local players = self:get_players(true) local players = self:get_players(true)
for _, player in pairs(players) do for _, player in pairs(players) do
player.print(message) player.print(message)
@@ -436,8 +447,8 @@ end
--- @param message LocalisedString --- @param message LocalisedString
function ExpRoles.print_to_roles(roles, message) function ExpRoles.print_to_roles(roles, message)
for _, role in pairs(roles) do for _, role in pairs(roles) do
role = ExpRoles.get_role_from_any(role) local resolved = ExpRoles.get_role_from_any(role)
if role then role:print(message) end if resolved then resolved:print(message) end
end end
end end
@@ -445,12 +456,12 @@ end
--- @param role string | number | ExpRoles.Role --- @param role string | number | ExpRoles.Role
--- @param message LocalisedString --- @param message LocalisedString
function ExpRoles.print_to_roles_higher(role, message) function ExpRoles.print_to_roles_higher(role, message)
role = ExpRoles.get_role_from_any(role) local resolved = ExpRoles.get_role_from_any(role)
if not role then return end if not resolved then return end
local roles = {} local roles = {}
for index, role_name in ipairs(ExpRoles.config.order) do for index, role_name in ipairs(ExpRoles.config.order) do
if index <= role.index and role_name ~= ExpRoles.get_default_role_name() then if index <= resolved.index and role_name ~= ExpRoles.get_default_role_name() then
roles[#roles + 1] = role_name roles[#roles + 1] = role_name
end end
end end
@@ -462,12 +473,12 @@ end
--- @param role string | number | ExpRoles.Role --- @param role string | number | ExpRoles.Role
--- @param message LocalisedString --- @param message LocalisedString
function ExpRoles.print_to_roles_lower(role, message) function ExpRoles.print_to_roles_lower(role, message)
role = ExpRoles.get_role_from_any(role) local resolved = ExpRoles.get_role_from_any(role)
if not role then return end if not resolved then return end
local roles = {} local roles = {}
for index, role_name in ipairs(ExpRoles.config.order) do for index, role_name in ipairs(ExpRoles.config.order) do
if index >= role.index and role_name ~= ExpRoles.get_default_role_name() then if index >= resolved.index and role_name ~= ExpRoles.get_default_role_name() then
roles[#roles + 1] = role_name roles[#roles + 1] = role_name
end end
end end
@@ -521,7 +532,10 @@ local function emit_player_roles_updated(player, change_type, roles, by_player_n
end end
if not silent then if not silent then
game.print({ "exp-roles.game-message-" .. change_type, player.name, table.concat(role_names, ", "), by_player_name }) local joined = table.concat(role_names, ", ")
game.print(change_type == "assign"
and { "exp-roles.game-message-assign", player.name, joined, by_player_name }
or { "exp-roles.game-message-unassign", player.name, joined, by_player_name })
end end
if change_type == "assign" then if change_type == "assign" then
@@ -591,6 +605,7 @@ end
--- @param roles ExpRoles.Role[] --- @param roles ExpRoles.Role[]
--- @param change_type "assign" | "unassign" --- @param change_type "assign" | "unassign"
--- @param sync boolean --- @param sync boolean
--- @return ExpRoles.Role[] # The roles which actually changed
local function apply_local_change(player_name, roles, change_type, sync) local function apply_local_change(player_name, roles, change_type, sync)
local local_roles = script_data.local_players[player_name] or {} local local_roles = script_data.local_players[player_name] or {}
local pending = script_data.pending[player_name] or {} local pending = script_data.pending[player_name] or {}
@@ -860,7 +875,6 @@ end
]] ]]
--- Create persistent script data if this is the first startup --- Create persistent script data if this is the first startup
--- @package
function ExpRoles.on_server_startup() function ExpRoles.on_server_startup()
if compat.script_data["exp_roles"] == nil then if compat.script_data["exp_roles"] == nil then
--- @type ExpRoles.ScriptData --- @type ExpRoles.ScriptData
@@ -878,7 +892,6 @@ function ExpRoles.on_server_startup()
end end
--- Apply the flag triggers for a player who just joined --- Apply the flag triggers for a player who just joined
--- @package
--- @param event EventData.on_player_joined_game --- @param event EventData.on_player_joined_game
function ExpRoles.on_player_joined_game(event) function ExpRoles.on_player_joined_game(event)
local player = game.get_player(event.player_index) local player = game.get_player(event.player_index)