Tools

The midnight command has subcommands for working w/ Midnight projects:

CommandDoes
midnight compilecompiles Midnight files to Lua
midnight cleanremoves Lua files generated by midnight compile
midnight lintchecks Midnight files for likely mistakes
midnight fmtformats Midnight files

All of them take files and / or directories (directories are searched for *.mn files recursively, and default to .), accept flags before or after paths. compile and lint also take -markup-factory name (see markup). -h shows a command's usage.

compile

midnight compile [-o dir] [-f] [-p] [-markup-factory name] [paths]

Compiles foo.mn to foo.lua (next to it, or under -o dir, keeping the relative path). Same as the reference compiler's midnight compile:

The output is Lua 5.1 + goto (continue compiles to goto), w/ bit operators compiled to bit32 calls, so it runs on LuaJIT and Lua 5.2+ (or 5.1 w/ a bit32 polyfill). Statements are kept on their Midnight lines, so errors report Midnight line numbers.

clean

midnight clean [paths]

Removes .lua files that end w/ the -- __MIDNIGHT_COMPILED__ footer (from either midnight or the reference compiler). Other Lua files are never removed.

lint

midnight lint [-g list] [-markup-factory name] [paths | -]

Checks for likely mistakes. Explicitly named .lua files are checked as Lua, and - reads stdin.

RuleReports
undefined-globalreading a global that is never set (in the file)
set-globalsetting an undeclared global (ex. x = 1 or function f() without global / local)
unused-locala local (or local function, or destructured name) that is never read
shadowa local (or parameter, or loop variable) that shadows another local
redefineda local declared twice in the same scope
unreachablecode after break, continue, or goto (code after return is already a syntax error)
fat-arrow-componenta => function that builds markup elements but doesn't use self: probably a component, whose props would end up in self (use ->)
and-ora && b || c where b can be nil or false: it's then c even when a is true (use an if expression: if a { b } else { c }). Not reported when b can't be falsy (a literal, table, function, element, .., arithmetic, math.*, tostring, ...), when c is nil / false, or for safe navigation (x && x.y || d, x && x:m() || d, x && f(x) || d)
syntaxthe file doesn't parse

Not reported:

Globals are also read from the nearest .luarc.json (lua-language-server's config, looked up from each file's directory), so editor and linter agree:

{
  "workspace.library": ["../src/bots/lua_definitions"],
  "diagnostics.globals": ["provided_by_host"]
}

A comment containing midnight-lint: ignore silences its line:

print(provided_by_host) -- midnight-lint: ignore

Limitations: diagnostics have line numbers only (no columns), and destructured names w/ defaults always count as used (the default compiles to a nil check, which reads them).

The linter runs on the AST (the lint package), after Midnight is lowered to Lua.

Output

One line per problem, sorted by line:

path:line: message (rule)

Stdin is reported as <stdin>. Exits 1 if there are any problems, 0 otherwise.

fmt

midnight fmt [-check] [paths | -]

Formats files in place (printing the paths it changed), or stdin to stdout w/ -. -check doesn't write anything: it prints the paths of files that need formatting, and exits 1 if there are any.

The formatter works on tokens, not a syntax tree, so it keeps your line breaks (one line blocks stay on one line, and long lines aren't wrapped). It normalizes:

Strings (including interpolations and block strings) are never changed, and \r\n line endings become \n.

Every result is checked to parse to the same program as the input. If it doesn't (a formatter bug), the file is left unchanged and the error is reported. Files that don't parse are reported and left unchanged.

Output