WARFRAME Wiki
Advertisement
WARFRAME Wiki

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: Fri, 10 Sep 2021 06:52:35 +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, 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.
  • 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>').
  • 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" } ) }
    
Example
-- 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() and require 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
      
  • 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() or mw.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.

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 or function) as variable names.
    • Avoid using package/library names (e.g. math or table) 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, and t (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() or buildTable()).
    • Functions that cannot be {{#invoke:}}d on article pages (i.e it does not have frame parameter) should start with a single underscore (e.g. _getValue()) to distinguish them for functions that use the parser frame and frame.args to grab arguments from the {{#invoke:}} parser function. These functions are typically used in other modules.
  • 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.
  • self keyword refers to the object a method is invoked on (like this 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
      

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() or require keyword.
  • 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.
  • 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.
  • Limit nested code blocks to three at max. This includes for loops and if/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, 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: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.
  • Keep module sizes below 70k bytes or 1.5k lines. This is so our auto-documentation generator does not run out of memory when parsing these pages and to reduce code bloat.

Databases (/data) and Tables[]

  • 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 = {
        -- Links in string values that are meant to display to the user as is are okay
        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.
  • 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.
  • You are free to use mw.log() and mw.logObject() to output to the debug console, but please do not leave them in production code as they can be memory intensive (especially calling mw.logObject() on large tables).

Error Handling[]

  • Use assert(expression, message) and error(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 call pcall(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[]

Internationalization[]

Photo-4.png
“It's taking longer than I calculated.”
This page is actively being worked on and may not be completely correct. Please assist in making this page accurate.
Nothing in development or planning for i18n and i10n for the wiki as of 03:39, 31 May 2021 (UTC). This is not high on priority and would be a huge epic to tackle, estimating a 6-12 month duration as an on-and-off project. Feel free to contact User:Cephalon Scientia if you have some ideas or desire such a feature. Right now, our sister wikis just fork from this wiki's codebase and either 1. change the locale of data stored in /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.
  • 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 (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[]

Advertisement