diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b3059a..e2d606a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,6 +37,14 @@ jobs: version: v2.1.0 args: false # Will be run as part of `make check` + - name: Install lua-language-server + run: | + curl -sL "https://github.com/LuaLS/lua-language-server/releases/download/$LUALS/lua-language-server-$LUALS-linux-x64.tar.gz" \ + | tar -xz -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + env: + LUALS: 3.19.0 + - name: Check run: make check diff --git a/.luarc-typecheck.json b/.luarc-typecheck.json new file mode 100644 index 0000000..0a6ae01 --- /dev/null +++ b/.luarc-typecheck.json @@ -0,0 +1,20 @@ +{ + "runtime": { + "version": "LuaJIT" + }, + "workspace": { + "useGitIgnore": false, + "ignoreDir": [ + ".claude", + ".git", + ".idea", + ".install", + ".lua", + ".luarocks", + ".venv", + "build", + "dist", + "node_modules" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 629d976..e161ef9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,10 +50,27 @@ make format # Lint code make lint +# Check LuaCATS annotations with lua-language-server +make typecheck + # Build single-file distribution make build ``` +### typecheck + +`make typecheck` runs lua-language-server against a committed +`.luarc-typecheck.json` rather than whatever `.luarc.json` a developer has +locally. It catches what luacheck does not: undefined or duplicate `@alias`, +returns that disagree with `@return`, fields missing from a `@class`. + +`runtime.version` in that config is load-bearing. Left unset the server defaults +to Lua 5.4 and checks this library as the wrong language, which reports a +different set of findings rather than fewer. It is pinned to LuaJIT. + +Part of `check`, so CI enforces it. Clean under both 3.18.2 and 3.19.0; CI pins +3.19.0 because the findings do move between versions. + ## Architecture ### Module Design diff --git a/Makefile b/Makefile index 51f3f24..65ec4bd 100644 --- a/Makefile +++ b/Makefile @@ -152,8 +152,24 @@ lint: exit 1; \ fi +# Type-check annotations with the Lua language server. Catches what luacheck does +# not: undefined or duplicate `@alias`, returns that disagree with `@return`. +# +# `runtime.version` in the pinned config is load-bearing: unset, the server +# defaults to Lua 5.4 and checks this library as the wrong language. +.PHONY: typecheck +typecheck: + @if command -v lua-language-server >/dev/null 2>&1; then \ + echo "Running lua-language-server $$(lua-language-server --version)..."; \ + lua-language-server --check "$(CURDIR)" --checklevel=Warning \ + --configpath="$(CURDIR)/.luarc-typecheck.json" --logpath="$(CURDIR)/build/luals"; \ + else \ + echo "lua-language-server not found. Install with: make install-deps"; \ + exit 1; \ + fi + .PHONY: check -check: format-check lint +check: format-check lint typecheck @echo "Code quality checks complete." # Clean generated files @@ -186,6 +202,7 @@ help: @echo " make format - Format code with stylua" @echo " make format-check - Check code formatting" @echo " make lint - Lint code with luacheck" + @echo " make typecheck - Check annotations with lua-language-server" @echo "" @echo "Setup:" @echo " make install-deps - Install development dependencies" diff --git a/src/bitn/_compat.lua b/src/bitn/_compat.lua index 17b2ac6..cdbf6a4 100644 --- a/src/bitn/_compat.lua +++ b/src/bitn/_compat.lua @@ -143,10 +143,12 @@ end local bit_lib local is_luajit = false --- Try LuaJIT's bit library first -ok, result = pcall(require, "bit") -if ok and result then - bit_lib = result +-- Try LuaJIT's bit library first. Fresh locals rather than the pcall results +-- from the native-operator probe above: that `result` is a compiled chunk, this +-- one is a library table. +local bit_ok, bit_module = pcall(require, "bit") +if bit_ok and bit_module then + bit_lib = bit_module is_luajit = true else -- Try Lua 5.2's bit32 library (use rawget to avoid recursion with our module name) diff --git a/src/bitn/bit64.lua b/src/bitn/bit64.lua index eb59e31..dfefdd7 100644 --- a/src/bitn/bit64.lua +++ b/src/bitn/bit64.lua @@ -942,20 +942,23 @@ function bit64.selftest() for _, test in ipairs(test_vectors) do total = total + 1 local result = test.fn(unpack_fn(test.inputs)) + -- Held in a local so the type narrowing below applies to it; a table field + -- is not narrowed by a `type()` check. + local expected = test.expected - if type(test.expected) == "table" then + if type(expected) == "table" then -- 64-bit comparison - if eq64(result, test.expected) then + if eq64(result, expected) then print(" PASS: " .. test.name) passed = passed + 1 else print(" FAIL: " .. test.name) - print(" Expected: " .. fmt64(test.expected)) + print(" Expected: " .. fmt64(expected)) print(" Got: " .. fmt64(result)) end - elseif type(test.expected) == "string" then + elseif type(expected) == "string" then -- Byte string comparison - if result == test.expected then + if result == expected then print(" PASS: " .. test.name) passed = passed + 1 else @@ -965,8 +968,8 @@ function bit64.selftest() print(" Got: " .. type(result)) else local exp_hex, got_hex = "", "" - for i = 1, #test.expected do - exp_hex = exp_hex .. string.format("%02X", string.byte(test.expected, i)) + for i = 1, #expected do + exp_hex = exp_hex .. string.format("%02X", string.byte(expected, i)) end for i = 1, #result do got_hex = got_hex .. string.format("%02X", string.byte(result, i)) @@ -976,12 +979,12 @@ function bit64.selftest() end end else - if result == test.expected then + if result == expected then print(" PASS: " .. test.name) passed = passed + 1 else print(" FAIL: " .. test.name) - print(" Expected: " .. tostring(test.expected)) + print(" Expected: " .. tostring(expected)) print(" Got: " .. tostring(result)) end end