Skip to content

fix(convex-lint): scan to handler: instead of a fixed 300-char window - #15

Open
milehighideas wants to merge 1 commit into
get-convex:mainfrom
Mile-High-Ideas:fix/convex-lint-returns-window
Open

fix(convex-lint): scan to handler: instead of a fixed 300-char window#15
milehighideas wants to merge 1 commit into
get-convex:mainfrom
Mile-High-Ideas:fix/convex-lint-returns-window

Conversation

@milehighideas

Copy link
Copy Markdown

Fix false-positive "missing returns:" advisory in convex-lint.mjs

Repo: get-convex/convex-backend-skill
File: hooks/convex-lint.mjs

Problem

The PreToolUse convex-lint hook emits a false-positive advisory:

convex-lint: a `internalMutation({...})` in `.../foo.ts` appears to be missing
`returns:`. Convex functions should always declare argument and return
validators (use v.null() for functions that return nothing).

…on functions that do declare returns:. It fires whenever a function's
args: (and/or returns:) validator is large enough that the returns: key
sits more than ~300 characters after the fn({ opening.

Root cause

The soft-warning pass scans a fixed 300-character window after each
function-open match:

const head = projected.slice(m.index, m.index + 300);
if (!/\breturns\s*:/.test(head)) missing.push("`returns:`");

A function with a sizable args object (nested v.object, several optional
fields) pushes returns: past char 300, so the regex never sees it. Example
offsets from a real file: returns: at char 501 and 406 from the
respective internalMutation({ — both beyond the window, both falsely flagged.

Fix

Scan up to the function's handler: key instead of a fixed window. Every Convex
object-form function has exactly one handler:, declared after args/returns,
so the region before it is precisely the validator block — regardless of size.
Fall back to 300 chars if no handler: is found.

   while ((m = objectFormRe.exec(projected)) !== null) {
-    const head = projected.slice(m.index, m.index + 300);
+    // Scan the function's option object up to its `handler:` key (every Convex
+    // function object has one, declared after args/returns) instead of a
+    // fixed-size window. A large `args:`/`returns:` validator can push
+    // `returns:` well past a 300-char window, producing a false "missing
+    // `returns:`" advisory. Fall back to 300 chars if no handler is found.
+    const rest = projected.slice(m.index);
+    const handlerIdx = rest.search(/\bhandler\s*:/);
+    const head = handlerIdx === -1 ? rest.slice(0, 300) : rest.slice(0, handlerIdx);
     const missing = [];
     if (!/\bargs\s*:/.test(head)) missing.push("`args:`");
     if (!/\breturns\s*:/.test(head)) missing.push("`returns:`");

Verification

  • Function with a large args block and a returns: validator → no longer
    flagged (was a false positive before).
  • Function genuinely missing returns: (even with a large args block) → still
    correctly flagged (no false-negative regression).
  • Function with returns: v.null() → silent, as expected.

The change is confined to the advisory pass; the two hard-deny rules
(.filter on a db query, positional function syntax) are untouched.

The soft-warning pass scanned a fixed 300-char window after each `fn({`
to look for `args:`/`returns:`. A large `args:`/`returns:` validator
pushes `returns:` past the window, producing a false "missing `returns:`"
advisory on functions that do declare it.

Scan up to the function's `handler:` key instead (always present, and
declared after args/returns); fall back to 300 chars if none is found.
Hard-deny rules are untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants