2 luacheck, stylua, pre commit
flux edited this page 2023-05-20 17:32:20 +00:00

this is a guide for setting up luacheck, stylua, and pre-commit, which can help detect basic bugs in lua scripts.

luacheck

luacheck is available as a package on many linux distributions, and can also be installed via luarocks. the main documentation page is https://luacheck.readthedocs.io/en/stable/.

luacheck can be run on individual files, or on directories. commonly, you will call it on the current directory:

  • luacheck .

using the personal_log mod as an example, here is some output:

# flux@nail:~/.minetest/mods.your-land/personal_log
luacheck . 
Checking init.lua                                 158 warnings

    init.lua:1:1: setting non-standard global variable personal_log
    init.lua:3:17: accessing undefined variable minetest
    init.lua:4:7: unused variable modpath
    init.lua:4:17: accessing undefined variable minetest
    init.lua:6:26: accessing undefined variable minetest
...
    init.lua:859:4: line contains trailing whitespace
    init.lua:860:1: line contains only whitespace
    init.lua:861:1: mutating non-standard global variable personal_log

Total: 158 warnings / 0 errors in 1 file

clearly, some of these issues are spurious - the minetest variable is expected and we should allow access to it, and it's not an error to create and modify the personal_log variable. while there's a number of command line options we could supply to suppress these, a better option is to create a .luacheckrc file, which you place at the root of the repo. it should be added to the repo, so that all checkouts can use it. here is a simple example:

std = "lua51+luajit+personal_log"
unused_args = false
ignore = {"611", "612"}  -- ignore whitespace issues for the moment

stds.personal_log = {
    read_globals = {
    	"minetest",
        "mcl_formspec",
        "ItemStack",
        "ccompass",
        "dump",
        "vector",
        "unified_inventory",
        "sfinv_buttons",
        "sfinv",
        "mcl_util",
    },
    globals = {
        "personal_log",
    },
}

a more complicated example can be viewed at https://github.com/fluxionary/minetest-choppy/blob/main/.luacheckrc

now, the output is

luacheck .
Checking init.lua                                 22 warnings

    init.lua:4:7: unused variable modpath
    init.lua:63:2: setting non-standard global variable privs
    init.lua:114:121: line is too long (129 > 120)
    init.lua:115:121: line is too long (133 > 120)
    init.lua:116:121: line is too long (125 > 120)
    init.lua:117:121: line is too long (129 > 120)
    init.lua:186:121: line is too long (122 > 120)
    init.lua:212:121: line is too long (122 > 120)
    init.lua:213:121: line is too long (123 > 120)
    init.lua:239:121: line is too long (122 > 120)
    init.lua:295:8: unused variable prefix_start
    init.lua:342:5: 'not (x == y)' can be replaced by 'x ~= y' (if neither side is a table or NaN)
    init.lua:353:5: Error prone negation: negation is executed before relational operator.
    init.lua:517:121: line is too long (123 > 120)
    init.lua:533:33: accessing undefined variable core
    init.lua:533:121: line is too long (162 > 120)
    init.lua:535:9: shadowing definition of variable entry on line 519
    init.lua:659:121: line is too long (124 > 120)
    init.lua:671:121: line is too long (125 > 120)
    init.lua:687:10: shadowing definition of variable category on line 590
    init.lua:706:52: shadowing upvalue argument player on line 702
    init.lua:746:121: line is too long (121 > 120)

Total: 22 warnings / 0 errors in 1 file

i'll explain some of these warnings:

  • unused variable modpath

    this means that a variable was declared but never used, and should probably be removed or commented out.

  • setting non-standard global variable privs

    this variable is missing a local statement, and thus it is being leaked into global scope, possibly interfering w/ other mods, or possibly being accidentally or intentionally modified by other mods.

  • line is too long (129 > 120)

    i feel 120 is a reasonable maximum line length. if a line is longer than that, it's best to figure out how to break it up into smaller statements or at least re-format it to spread across multiple lines.

  • Error prone negation: negation is executed before relational operator.

    this is luacheck catching the not a == b pattern. note that it's not particularly good at catching this pattern - it seems to get confused by code like if not a or not a == b then. but at least it's something.

  • accessing undefined variable core

    the core namespace is, by convention, only accessed by minetest's internal lua code, and mods shouldn't be using or relying on it.

  • shadowing definition of variable entry on line 519

    this means that a variable named entry was defined, and there was already such a variable defined prior to this. this is not necessarily an error, but it's easy to confuse which variable is relevant in which scope, and you might end up mistaking one for the other.

stylua

stylua is an opinionated code formatter, which can also catch some simple bugs. the main page is the github repo, which also explains various ways it can be installed: https://github.com/JohnnyMorganz/StyLua.

stylua's main goal is to keep the style of the code consistent. it will also automatically reformat long lines. keeping and requiring a consistent style is very useful in reducing spurious info in diffs, related to e.g. whitespace. while stylua's opinions are not exactly my own opinions, i find that it's better to have some sort of standard, than none at all.

stylua does have a few options and a little configurability, but i only use the defaults.

if you do choose to adopt stylua for a project, it is probably a good idea to create a single commit which is just the result of running stylua on the codebase, without any other changes. this initial commit can be quite large, and it will hide any other changes. it's also a good idea to give the --verify argument on this first run, which verifies that the style changes do not change the semantics of the code.

pre-commit

pre-commit is a tool that can be used to enforce various checks before you commit code. if any of the checks fail, you must fix the problems before it will let you commit successfully. the main page for it is https://pre-commit.com/.

pre-commit is a python project, and most of its built-in checks are designed for other python projects. however, it also allows making use of external tools, and i will outline how to set it up to make use of luacheck and stylua.

arch linux has a pre-commit package, but i'm unsure which other distros have such. alternatively, it can be installed using python's pip.

there are two parts to setting up pre-commit in a repo. the first is to create a .pre-commit-config.yaml file, which should be added to the repo. here are the contents of the file that i use for nearly all of my projects:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.3.0
    hooks:
      - id: fix-byte-order-marker
      - id: end-of-file-fixer
      - id: trailing-whitespace
      - id: mixed-line-ending
        args: [ --fix=lf ]

  - repo: local
    hooks:
      - id: detect_debug
        name: detect debug
        language: pygrep
        entry: DEBUG
        pass_filenames: true
        exclude: .pre-commit-config.yaml
        fail_fast: true
      - id: stylua
        name: stylua
        language: system
        entry: stylua
        pass_filenames: true
        types: [ file, lua ]
        fail_fast: true
      - id: luacheck
        name: luacheck
        language: system
        entry: luacheck
        pass_filenames: true
        types: [ file, lua ]
        args: [ -q ]
        fail_fast: true

the first section, under repo: https://github.com/pre-commit/pre-commit-hooks, defines a number of automatic whitespace fixes. then there are 3 "external" checks. the first, under id: detect_debug, detects whether i've left any debug statements in the code (which i've been known to do). then, it runs stylua, then, luacheck. if any of these checks fail, the code cannot be committed. note that both the initial whitespace checks, and stylua, can modify code when they fail. in this case, you'd re-add the modified files, and run commit again:

# flux@nail:~/.minetest/mods.your-land/yl_commons
git add chatcommands/bailiff_votes.lua 	
# flux@nail:~/.minetest/mods.your-land/yl_commons
git commit -m "fix logic in bailif vote command"
fix UTF-8 byte order marker..............................................Passed
Fix End of Files.........................................................Passed
Trim Trailing Whitespace.................................................Passed
Mixed line ending........................................................Passed
detect debug.............................................................Passed
stylua...................................................................Failed
- hook id: stylua
- files were modified by this hook
# flux@nail:~/.minetest/mods.your-land/yl_commons
git add chatcommands/bailiff_votes.lua 	
# flux@nail:~/.minetest/mods.your-land/yl_commons
git commit -m "fix logic in bailif vote command"
fix UTF-8 byte order marker..............................................Passed
Fix End of Files.........................................................Passed
Trim Trailing Whitespace.................................................Passed
Mixed line ending........................................................Passed
detect debug.............................................................Passed
stylua...................................................................Passed
luacheck.................................................................Passed
[yl_stable 4a0e0f4] fix logic in bailif vote command
 2 files changed, 134 insertions(+), 128 deletions(-)

the second part of setting up pre-commit is actually installing its hooks in a repo. this is a step that has to be done separately for every checkout - git does not automatically know that you want to run pre-commit's hooks. this is fairly painless, though, you simply run pre-commit install while in the root of the target repo.

that's about it. hope this helps!