From 13243b54eb302c6d9626730bfb2d8b130ac2c665 Mon Sep 17 00:00:00 2001 From: Derek Miller <1340081+derek-miller@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:55:20 -0500 Subject: [PATCH 1/2] Gate typecheck in check, fix the AESWord annotation and the SRP group narrowing lua-crypto was the one library where typecheck was not part of check, so this class of defect had no CI gate here at all. Two real annotation defects fall out once it runs: aes_gcm.lua create_aes_word() returns a 4-element word but annotated its body @type AESState, which is [AESWord, AESWord, AESWord, AESWord]. Same copy-paste as the one fixed in lua-bthome-ble aes_ccm.lua. Clearing it takes the repo from 11 findings to 6. srp.lua read group.g three times across a type(group.g) branch. Narrowing applies to locals, not table fields, so neither arm was narrowed and both bignum.from_number and bignum.from_hex saw the full string|integer union. Reading it into a local once fixes both. Same root cause as the bit64 test.expected hoist in lua-bitn. The remaining four are deliberate negative tests that pass nil or an integer to assert the function rejects it, so the wrong type is the point. Those get disable-next-line rather than a signature change. The config carries the full bypass set the other four libraries now use, including runtime.plugin, which a grep of the diagnostics paths cannot surface: check_worker.lua does require "plugin", so an OnSetText returning an empty edit blanks every file and the check passes having analysed nothing. diagnostics.disable keeps its existing unnecessary-assert entry. CI installs lua-language-server 3.19.0 before Check, matching the other four. Full test suite passes, including the SRP change: AES, AES-GCM, ChaCha20, Poly1305, SHA-256/512, BLAKE2, HKDF, SRP, bignum, X25519, X448, Ed25519 and the OpenSSL gating. --- .github/workflows/build.yml | 8 +++++++ .luarc-typecheck.json | 43 +++++++++++++++++++++++++++++++++---- Makefile | 2 +- src/crypto/aes_gcm.lua | 2 +- src/crypto/ed25519.lua | 2 ++ src/crypto/srp.lua | 12 +++++++---- 6 files changed, 59 insertions(+), 10 deletions(-) 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 e902932..9061461 100644 --- a/.luarc-typecheck.json +++ b/.luarc-typecheck.json @@ -1,9 +1,44 @@ { - "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": { "disable": ["unnecessary-assert"] } + "diagnostics": { + "enable": true, + "disable": [ + "unnecessary-assert" + ], + "severity": {}, + "globals": [], + "globalsRegex": [], + "neededFileStatus": {}, + "groupFileStatus": {}, + "groupSeverity": {}, + "enableScheme": [ + "file" + ] + } } diff --git a/Makefile b/Makefile index 4610957..2cb586b 100644 --- a/Makefile +++ b/Makefile @@ -184,7 +184,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) From 382dc1594f90882b2af085865f5c3ab1f8ee2371 Mon Sep 17 00:00:00 2001 From: Derek Miller <1340081+derek-miller@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:36:54 -0500 Subject: [PATCH 2/2] Correct the Makefile comment that still deferred gating typecheck The paragraph above the typecheck target named the two categories this change resolves, type-narrowing and deliberate-bad-argument findings, and deferred the decision this change makes. Left alone it reads as a current known exception to anyone editing the file later, which is the same defect class #4 removed. --- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2cb586b..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