diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab80b3e..c583a6f 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 index a82e975..22f8a73 100644 --- a/.luarc-typecheck.json +++ b/.luarc-typecheck.json @@ -1,8 +1,42 @@ { - "runtime": { "version": "LuaJIT" }, + "runtime": { + "version": "LuaJIT", + "special": {}, + "plugin": "", + "pluginArgs": [] + }, "workspace": { "useGitIgnore": false, - "library": ["vendor"], - "ignoreDir": [".claude", ".git", ".idea", ".venv", "build", "dist", "node_modules", "vendor"] + "library": [ + "vendor" + ], + "ignoreDir": [ + ".claude", + ".git", + ".idea", + ".install", + ".lua", + ".luarocks", + ".venv", + "build", + "dist", + "node_modules", + "vendor" + ], + "maxPreload": 5000, + "preloadFileSize": 500 + }, + "diagnostics": { + "enable": true, + "disable": [], + "severity": {}, + "globals": [], + "globalsRegex": [], + "neededFileStatus": {}, + "groupFileStatus": {}, + "groupSeverity": {}, + "enableScheme": [ + "file" + ] } } diff --git a/Makefile b/Makefile index 4610957..befcc67 100644 --- a/Makefile +++ b/Makefile @@ -164,9 +164,10 @@ lint: # luacheck -- duplicate or undefined `@alias`, return counts that disagree with # `@return`, fields missing from a `@class` -- so it is a separate target. # -# Deliberately NOT part of `check`: a handful of type-narrowing and -# deliberate-bad-argument diagnostics remain, and turning CI red on those is a -# separate decision from making the check runnable. +# Part of `check`, so CI enforces it. The type-narrowing and +# deliberate-bad-argument findings that held it out are resolved: the narrowing +# ones were real defects, and the bad-argument ones are negative tests carrying +# a scoped `@diagnostic` bypass. # # Checks the whole repo, not just src/: an editor's workspace is the repo, and # @alias resolves workspace-wide, so a narrower scope gives different findings @@ -184,7 +185,7 @@ typecheck: fi .PHONY: check -check: format-check lint +check: format-check lint typecheck @echo "Code quality checks complete." # Clean generated files diff --git a/src/crypto/aes_gcm.lua b/src/crypto/aes_gcm.lua index 88572db..add1483 100644 --- a/src/crypto/aes_gcm.lua +++ b/src/crypto/aes_gcm.lua @@ -309,7 +309,7 @@ local RCON = { --- Initialize a 4-element AES word with zeros --- @return AESWord word Initialized word local function create_aes_word() - --- @type AESState + --- @type AESWord return { 0, 0, 0, 0 } end diff --git a/src/crypto/ed25519.lua b/src/crypto/ed25519.lua index 65050c1..cda11a9 100644 --- a/src/crypto/ed25519.lua +++ b/src/crypto/ed25519.lua @@ -1203,9 +1203,11 @@ function ed25519.selftest() name = "verify returns false for non-string arguments", test = function() local v = test_vectors[3] + --- @diagnostic disable: param-type-mismatch -- the wrong types are the test assert(ed25519.verify(nil, v.message, v.signature) == false, "nil public key must be rejected") assert(ed25519.verify(v.public_key, nil, v.signature) == false, "nil message must be rejected") assert(ed25519.verify(v.public_key, v.message, 42) == false, "non-string signature must be rejected") + --- @diagnostic enable: param-type-mismatch end, }, } diff --git a/src/crypto/srp.lua b/src/crypto/srp.lua index cc62704..2e3d794 100644 --- a/src/crypto/srp.lua +++ b/src/crypto/srp.lua @@ -137,12 +137,15 @@ local function resolve_group(group) local N = bignum.from_hex(group.N) assert(not bignum.is_zero(N), "SRP: group modulus N must be non-zero") + -- Read into a local because narrowing applies to locals, not table fields: + -- `type(group.g)` does not tell the checker anything about `group.g`. + local g_spec = group.g local g - if type(group.g) == "number" then - g = bignum.from_number(group.g) + if type(g_spec) == "number" then + g = bignum.from_number(g_spec) else - assert(type(group.g) == "string", "SRP: group.g must be a number or a hex string") - g = bignum.from_hex(group.g) + assert(type(g_spec) == "string", "SRP: group.g must be a number or a hex string") + g = bignum.from_hex(g_spec) end assert(not bignum.is_zero(g), "SRP: group generator g must be non-zero") @@ -790,6 +793,7 @@ function srp.selftest() end) check("verify rejects a non-string M2", function() + --- @diagnostic disable-next-line: param-type-mismatch -- the wrong type is the test return reference_session:verify(nil) == false end)