Similar to WARFRAME Wiki:Styling Guide for article pages, this article aims to establish a coding convention for Lua scripts and tables in Module pages for consistency, readability, maintainability, and ease of editing. As this wiki is a community-based and open-source project, it is expected that contributors follow these guidelines and best practices to ensure the longevity of the wiki's codebase. Note that these guidelines are not strictly enforced so feel free to deviate from them if it feels appropriate to to do so. For the quickest response time from our community developers, you can reach out in #wiki_talk
of the wiki's Discord channel.
- Last updated: Wed, 17 May 2023 17:37:46 +0000 (UTC) by User:Cephalon Scientia
Style Guide[]
Modules and submodules should generally follow conventions established by lua-users.org with some changes since these scripts are written in the context of running on a MediaWiki-based wiki.
Above all styling guidelines: Be consistent, write readable code, and follow the convention established within the module you are editing.
Formatting[]
- Indentation should use tabs and tabs only. Indentation depth must be consistent throughout the entire module.
- A reason why tabs should be used over spaces because wiki pages have a default maximum size of 2,147,483,647 bytes. This size restriction is more relevant to
/data
subpages that act as databases for wiki content so we should use tabs to save bytes.
- A reason why tabs should be used over spaces because wiki pages have a default maximum size of 2,147,483,647 bytes. This size restriction is more relevant to
- Try to limit statement lengths within 80 characters for visibility. If line wrapping is absolutely necessary, the Source Editor automatically indents the next statement to make it clear that a statement is line wrapping instead of being two separate statements.
- For strings that exceed this limit use multi-line string tokens:
local str = [[ This is a multi-line string. ]] -- Alternatively, if your string value contains -- "[[" and "]]" then expand it with equal signs: local str = [=[ This is a multi-line string, but it has [[ something with square brackets around it ]]. This is needed when denoting wikitext links in multi-line strings like: [[WARFRAME]]. ]=]
- Note that the first newline character immediately after the first multi-line string delimiter (e.g.
[[
) will be ignored, but the newline character before the last delimiter (e.g.]]
) will be included.
- Note that the first newline character immediately after the first multi-line string delimiter (e.g.
- For strings that exceed this limit use multi-line string tokens:
- Semicolons at the end of statements are optional. Most of the time they are not used, but if they are, use it throughout the module.
- When denoting strings, stick to one quote style throughout the module (i.e. single quotations
'
or double quotations"
).- A valid exception would be if a string represents tags in wikitext with CSS attributes (e.g.
local result = '<div style="text-align:center;">Test</div>'
).
- A valid exception would be if a string represents tags in wikitext with CSS attributes (e.g.
- Avoid writing "one-liners" which are code blocks that are compressed into one line. These are often times hard to follow and maintain, especially with nested delimiters. Instead, expand these onto multiple lines with proper indentation.
-- Instead of writing this: local function printAndIncrement(t) for k, v in pairs(t) do print(v); t[k] = v + 1 end return t end -- Write: local function printAndIncrement(t) for k, v in pairs(t) do print(v) t[k] = v + 1 end return t end
- Add a single space after commas (
,
) and semicolons (;
) before continuing the code statement for readability. - Add a single space between two different brackets/parentheses for readability, especially within the source editor.
-- Instead of writing this: {returnsAFunctionTable()["functionName"]({key = "value"})} -- Write: { returnsAFunctionTable()["functionName"]( { key = "value" } ) } -- Or: { returnsAFunctionTable() ["functionName"] ( { key = "value" } ) }
-- As seen in Module:Tooltips/tip, maps shorthand polarity name to polarity image.
-- Do not program everything like this normally. In this scenario, the code is still
-- readable, just not at first glance due to nested brackets. Adding additional
-- comments will help with code comprehension.
function getPolarityImage(pol)
return ({
Zenurik = "Zenurik_Pol_W.png",
Naramon = "Naramon_Pol_W.png",
Vazarin = "Vazarin_Pol_W.png",
Penjaga = "Penjaga_Pol_W.png",
Madurai = "Madurai_Pol_W.png",
Koneksi = "Koneksi_Pol_W.png",
Umbra = "Umbra_Pol_W.png",
Unairu = "Unairu_Pol_W.png",
Exilus = "Exilus_W.png",
Aura = "Aura_Pol_White.png",
None = "Spacer.png",
})[({
Ability = 'Zenurik',
Bar = 'Naramon',
D = 'Vazarin',
Y = 'Penjaga', Sila = 'Penjaga', Sentinel = 'Penjaga', Precept = 'Penjaga',
V = 'Madurai',
O = 'Koneksi', Core = 'Koneksi', Fusion = 'Koneksi', Pengetikan = 'Koneksi',
U = 'Umbra', Q = 'Umbra',
Ward = 'Unairu', R = 'Unairu',
})[pol]]
end
- Add a single space before and after equal signs (
=
) when assigning variables. - Stick to one way of using
mw.loadData()
andrequire
when importing modules/packages/libraries. This guideline also applies to all function calls with a single string or table argument:-- Using standard function notation local Math = mw.loadData('Module:Math') local Math = require('Module:Math') -- Syntactic sugar: -- Omitting optional parentheses in function call local Math = mw.loadData 'Module:Math' local Math = require 'Module:Math' -- Syntactic sugar: -- Omitting optional parentheses and using multi-string tokens -- to make argument look like an interwiki link local Math = mw.loadData [[Module:Math]] local Math = require [[Module:Math]]
- Take advantage of Lua's
string.format()
for a feature similar to string interpolation. Use this over concatenating many string values as it makes code easier to read and maintain:-- Instead of writing this: function buildSimpleGallery(image, link, caption) local gallery = '<gallery widths="200" hideaddbutton="true" captionalign="center" spacing="small" bordercolor="transparent">\n' local galleryEntry = image.."|link="..link.."|''"..caption.."''" return gallery..galleryEntry..'\n</gallery>' end -- Write: function buildSimpleGallery(image, link, caption) local gallery = '<gallery widths="200" hideaddbutton="true" captionalign="center" spacing="small" bordercolor="transparent">\n' -- Variable values will replace "%s" placeholder string tokens local galleryEntry = ("%s|link=%s|''%s''"):format(image, link, caption) -- Alternatively: string.format("%s|link=%s|''%s''", image, link, caption) -- Other tokens can be used, such as %d for whole numbers or %f for floats (%.2f rounds to two digits after the decimal point) return gallery..galleryEntry..'\n</gallery>' end
- Avoid making many simple if/else code blocks for variable assignments or return statements. These can be condensed using
and
/or
as ternary operations.- Take advantage of short circuiting techniques when doing so.
-- Instead of writing this: function getArgFromFrame(frame) local arg1 = "" -- Alternatively: if not (frame and frame.args and frame.args[1]) then if frame == nil or frame.args == nil or frame.args[1] == nil then arg1 = "Placeholder" else arg1 = frame.args[1] end print("First argument:", arg1) return arg1 end -- Write: function getArgFromFrame(frame) -- Interpret this as if either frame or frame.args are not nil, -- use frame.args[1] value; -- if frame.args[1] value is nil, use "Placeholder"; -- if frame or frame.args are nil, use "Placeholder" local arg1 = ((frame and frame.args) and frame.args[1]) or "Placeholder" print("First argument:", arg1) return arg1 end
- Take advantage of short circuiting techniques when doing so.
- Module imports should come before any other code statements or blocks and should exist at the very top of the function for readability and maintainability. Putting
require()
ormw.loadData()
within functions may help with performance if they are only called zero or one times, but generally it is better to just import everything at once before calling functions of those modules.- See this for a discussion on the topic: https://stackoverflow.com/questions/128478/should-import-statements-always-be-at-the-top-of-a-module
- For syntax similar to named parameters in function calls, one could take advantage of optional parentheses (
( )
) for function calls and pass in a table containing keys mapped to argument values. Note that passing in a Frame object to functions act similar to this since additional arguments from{{#invoke:}}
call is stored inframe.args
table.function myProfile(firstName, lastName, birthday, gender, phoneNumber, email) -- Do something with arguments here end myProfile("John", "Smith", nil, "Male", nil, "john_smith@example.com") -- Alternatively, using ... varargs to pass in table argument (could replace with a single named argument) function myProfile(...) -- Extract arguments from table or use table as is -- Do something with arguments here end myProfile({ email = "john_smith@example.com", firstName= "John", lastName = "Smith" }) -- Or omit optional parentheses and use curly braces to mimic them for syntactic sugar myProfile{ email = "john_smith@example.com", firstName= "John", lastName = "Smith" }
Naming[]
- Value and object (table) variable naming — Keep variable names as consise yet descriptive as possible. Use
camelCasing
if variable name has more than one word.- For booleans variables, name them according to the state they represent.
p
is used to denote a package holding all the public/exported functions of a module.- Avoid using reserved keywords (e.g.
local
orfunction
) as variable names. - Avoid using package/library names (e.g.
math
ortable
) as variable names. - The variable consisting of only an underscore ("_") is commonly used as a placeholder when you want to ignore the variable:
for _, v in ipairs(t) do print(v) end
i
,k
,v
, andt
(representing "index", "key", "value", and "table" respectively) are often used as follows:for k, v in pairs(t) <content> end for i, v in ipairs(t) <content> end mt.__newindex = function(t, k, v) <content> end
- If tables are treated as objects, storing state (key-value pairs) and/or behavior (functions), then they are usually in
PascalCase
.-- No need to capitalize, simple list local list = { "This", "is", "a", "list" } -- Capitalize the variable name since it has key-value pairs -- and a function local DataEntry = { name = "Test", isActive = true, printElem = function(elem) print(elem) end } -- Importing other modules w/ exported functions local Math = require("Module:Math")
- Function naming — Functions follow similar rules to value and object variable naming, being first class objects. Function names use
camelCase
and should start with a verb (e.g.getValue()
orbuildTable()
).- Functions that cannot be
{{#invoke:}}
d on article pages (i.e it does not haveframe
parameter) should start with a single underscore (e.g._getValue()
) to distinguish them for functions that use the parserframe
andframe.args
to grab arguments from the{{#invoke:}}
parser function. These functions are typically used in other modules.
- Functions that cannot be
- Lua internal variable naming — By convention, names starting with an underscore followed by uppercase letters (such as
_VERSION
) are reserved for internal global variables used by Lua. - Metamethods naming — By convention, names starting with double underscores (e.g.
__add
) are used to denote metamethods which changes how Lua performs an operation on a particular item (such as defining how to add two tables). - Constants naming — By convention, constants are named in
SCREAMING_SNAKE_CASE
, with words separated by underscores. Lua does not natively support immutable variables so either trust that end-users do not modify constants or put them in a read-only table. - Module/package/library naming — module names use
PascalCasing
such as Module:DamageTypes.- Submodules should reside as a subpage of the main module page (e.g.
Module:PAGENAME/<submodule>
). - Databases in the form of Lua tables should reside in the
/data
subpage (e.g.Module:PAGENAME/data
). - Test cases should reside in the
/testcases
subpage (e.g.Module:PAGENAME/testcases
) to be recognized by Module:TestHarness.
- Submodules should reside as a subpage of the main module page (e.g.
self
keyword refers to the object a method is invoked on (likethis
in C++ or Java). This is enforced by the colon (:
) syntactic sugar.local Car = { position = 10 } function Car:move(distance) self.position = self.position + distance end -- The above function is equilvalent to the following code snippet: function Car.move(self, distance) self.position = self.position + distance end
Commenting[]
- Every commented text should have space after the inline comment delimiter (
--
). - Every module and exported function should have at least one line of comments describing its use or purpose. Though we prefer you follow LuaDoc conventions for our document generator Module:Docbunto. Note that Docbunto code items are introduced by a block comment (
--[[ ]]--
), an inline comment with three hyphens (---
), or an inline@tag
comment.--- '''Math''' is an extension of the math STL, containing additional functionality -- and support. -- -- On this Wiki, Math is used in: -- * [[Module:Acquisition]] -- * [[Module:Shared]] -- * [[Module:Warframes]] -- * [[Module:Weapons]] -- * [[Template:Math]] -- -- @module math -- @alias p -- @author [[User:FINNER|FINNER]] -- @image MathLogo.png -- @require [[w:c:dev:Module:Arguments|Module:Arguments]] -- @require [[w:c:dev:Module:Entrypoint|Module:Entrypoint]] -- @require [[w:c:dev:Module:User error|Module:User error]] -- @release stable -- <nowiki> --- Evaluates input -- @function p.eval -- @param {string} num The input expression -- @return {number} eval(num) function p.eval(...) local args = getArgs({...}, {noEval = true}); if not(args[1]) then return userError('Not enough input arguments | eval(num)'); end return eval(args[1]); end
Development Standards[]
- For a full development guide beyond standard practices, see WARFRAME Wiki:Development Guide.
Design[]
Functions[]
- Functions should have one job and one job only. Break up larger functions into smaller ones whenever possible for modularity and reusability.
- Use existing functions instead of remaking new ones with the same functionality by importing existing modules/packages/libraries through
mw.loadData()
orrequire
keyword.- See Module:Math and Module:String for extended functionality of standard Lua libraries.
- Keep function definitions below 5 parameters. If you need more arguments, there are two ways to approach this problem using
unpack()
:- Pass in an argument table containing all of your arguments.
function add(frame) -- frame takes on the form { <metadata>, args = { <arguments> } } -- if passing arguments through {{#invoke:}} local x, y = unpack(frame.args) return x + y end
- Add
...
to the end of argument list to denote a variadic or vararg function.function add(...) -- unpack() returns a tuple of arguments local x, y = unpack({...}) return x + y end
- See https://www.lua.org/pil/5.1.htmlf for more information.
- Pass in an argument table containing all of your arguments.
- Exported functions will almost always have the
frame
object whose arguments passed through by{{#invoke:}}
can be accessed in the following table:frame.args
. - If possible, return the modified original input argument to support function chaining for local and/or exported functions.
- Limit the use of recursive functions or extremely long loops in modules. These scripts have limited allocation time and memory to execute, otherwise an error like Lua error: the time allocated for running scripts has expired or Lua error: not enough memory will be shown.
- Avoid creating nested functions above one level. In other words, (lambda or anonymous) functions in a function should not define new functions in them.
- Use Module:Arguments to handle arguments that are passed in via templates that use
{{#invoke:}}
. The library sanitizes arguments and removes empty strings that are typically used as default arguments when no value is passed in for a parameter.local Args = require('Dev:Arguments') function example(frame) local sanitizedArgs = Args.getArgs(frame.args or frame) end
- Surround a function with parentheses (
( )
) if you want to only use the first argument that it returns.- See https://www.lua.org/manual/5.1/manual.html#2.5 for more details
- Limit nested code blocks to three at max. This includes
for
loops andif/else
blocks. Any longer nested code blocks can usually be refactored into different helper functions or combined into a singular code block for conditional checks. - Whenever possible, always use native functions from modules rather than relying on
frame:preprocess()
orframe:callParserFunction()
to expand wikitext (e.g. templates, parser functions, and parameters such as{{{1}}}
) in string results. This will typically improve function performance.- However, if you have to,
frame:callParserFunction()
is preferable toframe:preprocess()
.
- However, if you have to,
Modules[]
- Modules should only pertain to one type of functionality and have limited scope. Generic functions should be relocated to Module:Shared, Module:Math, Module:String, Module:Table, or Module:Lua.
- Modules typically follow a Module-Template design pattern where a Template page invokes functions from the module and article pages will transclude templates to display information generated by modules.
- Examples include Module:Math/Template:Math and Module:Weapons/characteristics/Template:Advantages
- This is to decouple articles from modules in a separations of concern design. If an
{{#invoke:}}
call needs to be updated due to changes to implementation (e.g. moving functions to different modules or new function signature), we will not have to replace all{{#invoke:}}
calls that exist on pages. A single update to the{{#invoke:}}
calls in the desired template is sufficient for site-wide changes.
- Most of the modules follow Object-oriented programming (OOP) techniques and convention. Typically, OOP increases modularity and reusability of code, making codebases easier to manage.
- The modules themselves can be considered as objects when imported in other modules, however, they cannot store state and are immutable.
- Typically, we do not explicitly define classes (using tables and metatables in Lua) mostly because of legacy code not being written to support that. Often times class-like structures are not needed as all of our modules store functions and state is stored in
/data
subpages.
- Use local scope whenever possible. Too many global variables can potentially cause problems and add complexity, especially when modified and used in different functions.
- Keep module sizes below 70k bytes or 1.5k lines, including comments. This is so our auto-documentation generator (Docbunto) does not run out of memory when parsing these pages and to reduce code bloat.
Databases (/data
) and Tables[]
- Main article: WARFRAME Wiki:Updating Databases#For Developers
- All of our
/data
subpages contain Lua tables that store values in key-value pairs. They are structured like NoSQL key-value store databases. - All key names should be in
TitleCase
. - Values should be one type for a particular key name for consistency. Note that
nil
values are okay. - Keys that store article names or interwiki links should only have the full name of the article with no additional wikitext. If you want to add additional formatting, that would be done in the modules or templates.
local t = { -- Avoid adding wikitext links in string values that are meant to display to the user as is Description = "Radial Blind Augment: [[Blind|Blinded]] enemies take 300% more [[finisher]] damage", Image = "Panel.png", -- Instead of "[[File:Panel.png]]" or "File:Panel.png|30px" Link = "Damage/Impact Damage" -- Instead of "[[Damage/Impact Damage]]" or "Damage/Impact Damage|Impact" }
- If possible, map values to a key for faster access, especially when tables get extremely big. Internally, Lua implements table types as hash tables.[1]
- Table values should only contain data of one data type (e.g. table of strings or numbers, but not both). This is for consistency when accessing data. For example, if we are looping over a table and performing an operation, we should expect tables contain values of the same type so we do not need to type check every single value to avoid errors.
- If a table stores functions, all functions must share the same parameters (name and type) for consistency.
- Tables should not be both array-like and dictionary-like. Stick to one usage of tables for consistent behavior when iterating over values.
Architecture[]
General architecture of module pages:
M:<name> — invokable by articles/the main module M:<name>/testcases — unit tests M:<name>/data — Lua table database (optional) M:<name>/data/validate — data validation script/unit tests (required if /data exists) M:<name>/dev — development/sandbox (optional) M:<name>/<additional_subpages> — misc submodules (optional) Most, if not all, pages/subpages will have /doc for documentation
Documentation[]
- Each module and submodule has their own
/doc
subpage that will automatically be transcluded in the relevant module or submodule. These subpages should transclude Template:Docbunto for the auto-doc generator Module:Docbunto.- See https://dev.fandom.com/wiki/Global_Lua_Modules/Docbunto for documentation on markup language used for auto-doc.
- Every function should have a LuaDoc-style comment before them, describing the function's functionality, parameters, and what the function returns.
- Any inline comments should describe why or how something is used and not what something is doing. The "what" can be interpreted by reading the actual source code.
- By extension, unclear or unintuitive code statements/blocks should have a comment before them describing their purpose or why they are needed. Use your best judgment when adding these, not everything needs to be commented.
- If documenting problems with the code or features that you want to see, please add
TODO
comments in the desired location. - If a function is meant to be invoked (by passing in a
frame
argument), add the name of the article(s) where it is invoked to the documentation. Leaving these "breadcrumbs" will help future developers know where functions are used and where to check if something broke.
Testing[]
- Each module and submodule has their own
/testcases
subpage that will contain a Lua table with test parameters and expected outputs. - Each publicly exposed function should have at least one test to prove its intended functionality.
- You are free to use
mw.log()
andmw.logObject()
to output to the debug console, but please do not leave them in production code as they can be memory intensive (especially callingmw.logObject()
on large tables).
Error Handling[]
- Use
assert(expression, message)
anderror(message [, level])
to handle runtime errors. Be sure to output descriptive text so users could understand what they are doing incorrectly when invoking a function or calling a template that uses functions from modules. Error messages should also specify what function is returning that message and the arguments that are passed for easier debugging.- These functions will return a stack traceback which is useful for debugging purposes.
- Errors will be in the style of bolded, red text that is wrapped around by a span tag with class
"scribunto-error"
.-- Example usage of assert() and error(): function findElem(t, search) assert(type(t) ~= "table", "printArg(t): argument has to be a table") for _, elem in ipairs(t) do if elem == search then return true end end error("printArg(t): cannot find element "..search.." in table") end
- For performance reasons, avoid using
assert()
. See https://devforum.roblox.com/t/be-careful-when-using-assert-and-why/1289175 for a great write up on the topic.
- If you want code to continue running after running into an error (similar to a
try/catch
block), use protected callpcall(function, arg1, ...)
. When an error occurs,pcall()
will return just the status of the error.- Do not use this unless absolute necessary. Having too many protected calls makes it difficult to debug when there are functionality issues since errors are silently handled.
- If you want to set an error handler, use
xpcall(function, error handler function)
. You have to wrap your desired function call in another function that takes in no arguments. - Error messages should have a link to a site maintenance category in the form of
[[Category:Pages with <certain error>]]
. For most purposes, the default Category:Pages with script errors should suffice.
Presentation[]
- Any function that outputs text to the reader should follow guidelines outlined in WARFRAME_Wiki:Styling_Guide#Writing.
Internationalization[]
- Module should be able to account for different locales of our sister wikis. This is primarily done through Module:I18n with localized messages stored in WARFRAME Wiki:L10n.
- Thrown errors should be in the wiki's locale.
- See Dev_Wiki:Internationalization for more details on the project.
Contributions[]
- All edits to modules and submodules will assume that they were made in good faith. As long as contributed code does not break existing functionality, we have loose enforcement on who and how contributions are made.
- If you follow the development standards above then your code should be of high quality to be used on our wiki and in our sister wikis.
- Please settle disputes or issues in a respectful and friendly demeanor. If needed, an Administrator or Moderator can arbitrate.
- Expect your code to be edited or viewed by other contributors. All changes are made public through Special:RecentChanges and Special:RecentChangesLinked. As such, no one truly has individual ownership of code contributed on the wiki and as a community we make this wiki better, one line at a time.
- Major refactorings of working code should be done in the separate
/dev
subpage or user page. This is so that articles that use these modules will still be readable by other people. We do not want to "push broken code to production". - If fixes cannot be made in a timely manner (say within an hour or so), copy/paste broken code into a separate
/dev
subpage or user page and revert back changes to the recent stable release. - Every contributor is expected to follow or understand the above guidelines. If you have any questions or concerns please feel free to contact the wiki's Administrators or Moderators.
References[]
- Software quality on Wikipedia
- Lua (programming language) on Wikipedia
- https://www.lua.org/manual/5.1/ - Lua 5.1 manual
- https://www.mediawiki.org/wiki/Manual:Coding_conventions (Lua)
- https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual - Lua reference manual for Scribunto
- https://dev.fandom.com/wiki/Dev_Wiki:Coding_conventions - Lua coding convention on Fandom Developers wiki
- http://lua-users.org/wiki/LuaStyleGuide - Lua Style Guide
- https://stevedonovan.github.io/ldoc/manual/doc.md.html - LDoc manual
- Module:Math and Module:Math/doc for an example of a module that follows close to these guidelines.
- https://paulwatt526.github.io/wattageTileEngineDocs/luaOopPrimer.html - Examples of Object-Oriented Programming (OOP) in Lua
/data
subpages with some modifications to module pages or 2. keep locale of data stored in/data
subpages with some modifications to module pages and another module that deals with localization. 03:00, 20 October 2021 (UTC) update: Proof of concept on module i18n can be seen on Module:Math and Module:Math/i18n. See Special:BlankPage/I18nEdit for translation editor.