Join the RoClicks Discord
RoClicks

luau_check.py

Python, 58 lines from Building overview

Download luau_check.py
#!/usr/bin/env python3
"""Syntax-check every .luau file under a folder with the standalone `luau` CLI, without running it.

Why: pushing a script with a syntax error into Studio fails quietly (the push "succeeds"; the error
only shows in the output log when something requires it). Checking on disk first takes a second and
catches typos, unbalanced `end`s, and Luau's "Ambiguous syntax" error when a line starts with `(`.

How it works: each file is copied to a temp file with `do return end` prepended, so the CLI parses
and compiles the whole file but executes nothing (no Roblox globals needed). Line numbers in the
output are shifted back by one to match your file. A leading `--!` directive line is kept first
(errors on that line itself would be reported as line 1 either way).

Install the CLI from https://github.com/luau-lang/luau/releases (or `brew install luau`).
Usage: python3 luau_check.py [folder ...]     (default: current folder)
Exit code 1 if any file has errors, so it can gate a push script or CI.
"""
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path


def main():
    if not shutil.which("luau"):
        sys.exit("the `luau` CLI is not on PATH")
    roots = [Path(a) for a in sys.argv[1:]] or [Path(".")]
    files = sorted(f for r in roots for f in (r.rglob("*.luau") if r.is_dir() else [r]))
    bad = 0
    with tempfile.TemporaryDirectory() as td:
        tmp = Path(td) / "check.luau"
        for f in files:
            lines = f.read_text().split("\n")
            # keep a --! directive on line 1 (e.g. --!strict) so it still applies
            if lines and lines[0].startswith("--!"):
                text = lines[0] + "\ndo return end\n" + "\n".join(lines[1:])
            else:
                text = "do return end\n" + "\n".join(lines)
            shift = 1
            tmp.write_text(text)
            r = subprocess.run(["luau", str(tmp)], capture_output=True, text=True)
            out = (r.stdout + r.stderr).strip()
            if not out:
                continue
            bad += 1
            out = out.replace(str(tmp), str(f))
            out = re.sub(rf"{re.escape(str(f))}:(\d+)",
                         lambda m: f"{f}:{max(1, int(m.group(1)) - shift)}", out)
            out = re.sub(r"\bline (\d+)", lambda m: f"line {max(1, int(m.group(1)) - shift)}", out)
            print(f"--- {f}\n{out}")
    print(f"{len(files)} file(s), {bad} with errors")
    sys.exit(1 if bad else 0)


if __name__ == "__main__":
    main()