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.
- Last updated: Sun, 13 Jun 2021 20:02:10 +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. Indentation must be consistent throughout the entire module.
- Stick to using one indentation character thoughout the module (i.e. tabs or spaces, preferably tabs however).
- Try to limit statement lengths within 80 characters for visibility. If line wrapping is absolutely necessary, indent 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]]. ]=]
- 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.
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 tokens local galleryEntry = string.format("%s|link=%s|''%s''", image, link, caption) -- Alternatively: ("%s|link=%s|''%s''").format(image, link, caption) 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
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 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.- Most likely you will not see this paradigm in our modules.
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
- Most likely you will not see this paradigm in our modules.
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
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.
- When building wikitext to be displayed on an article, be sure to add a
mw.log()
that outputs the resultant wikitext. This is for wiki maintainers to copy and paste the wikitext in archived pages where a module may not support them anymore.- Alternatively, one could use Special:ExpandTemplates to expand templates and parser function calls into raw HTML.
- 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()
to expand wikitext (e.g. templates, parser functions, and parameters such as{{{1}}}
in string results. This is to improve function performance.
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, or Module:Table.
- 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:Tooltips/Template:Tooltip
- 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.
Databases (/data
)
- 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 = { 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" }
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. - Every function should have a LuaDoc-style comment before them, describing the function's functionality, parameters, and what the function returns.
- 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.
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.
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.
-- 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
- 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.
Internationalization
- Module should be able to account for different locales of our sister wikis. This is primarily done through M:I18n.
- 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, 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/
- https://www.mediawiki.org/wiki/Manual:Coding_conventions (Lua)
- https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual
- https://dev.fandom.com/wiki/Dev_Wiki:Coding_conventions
- http://lua-users.org/wiki/LuaStyleGuide
- https://stevedonovan.github.io/ldoc/manual/doc.md.html
- Module:Math and Module:Math/doc for an example of a module that follows close to these guidelines.
/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.