From c73aab175a94f7abc72201aa32a7bfc3728401af Mon Sep 17 00:00:00 2001 From: Marcelo Farias Date: Wed, 20 May 2026 09:17:07 -0300 Subject: [PATCH 1/2] fix(cap-check,intent-check): scan from bodyTokenStart to avoid false positives on parameter/return types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CAP001/CAP002 (cap-check.ts) and INT002 (intent-check.ts) were scanning from fn.tokenStart, which includes the fn keyword, name, parameter list, and return type annotation. This caused false positives when a stdlib namespace identifier (http, fs, time, …) appeared in a type annotation rather than an actual capability call — for example: fn handleReq(client: http.Client) -> string = "ok" // ^^ CAP001 fired on http.Client even though no capability is consumed The fix mirrors the approach from PR #71 (dep-check) and the thr002 branch (thr-check): start the scan at fn.bodyTokenStart ?? fn.tokenStart so the parameter list and return type annotation are skipped entirely. Adds regression tests to cap-check.test.ts confirming stdlib namespace in parameter type annotations no longer triggers CAP001. Co-Authored-By: Claude Sonnet 4.6 --- packages/compiler/src/passes/cap-check.ts | 4 +-- packages/compiler/src/passes/intent-check.ts | 2 +- packages/compiler/tests/cap-check.test.ts | 28 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/passes/cap-check.ts b/packages/compiler/src/passes/cap-check.ts index e1b3cbd..a6ab354 100644 --- a/packages/compiler/src/passes/cap-check.ts +++ b/packages/compiler/src/passes/cap-check.ts @@ -135,7 +135,7 @@ function checkDirect(src: string, allowGenerics: boolean): string { function checkDirectFn(src: string, tokens: Token[], fn: FnDecl, inner: FnDecl[]): void { const declared = new Set(fn.capabilities); - for (let i = fn.tokenStart; i < fn.tokenEnd; i++) { + for (let i = fn.bodyTokenStart ?? fn.tokenStart; i < fn.tokenEnd; i++) { if (insideAny(i, inner)) continue; const tok = tokens[i]; if (!tok || tok.kind !== "ident") continue; @@ -290,7 +290,7 @@ function scanBody( const callNames = new Set(); const fnNames = new Set(decls.map((d) => d.name)); - for (let i = fn.tokenStart; i < fn.tokenEnd; i++) { + for (let i = fn.bodyTokenStart ?? fn.tokenStart; i < fn.tokenEnd; i++) { if (insideAny(i, inner)) continue; const tok = tokens[i]; if (!tok || tok.kind !== "ident") continue; diff --git a/packages/compiler/src/passes/intent-check.ts b/packages/compiler/src/passes/intent-check.ts index 985d706..71d8c9d 100644 --- a/packages/compiler/src/passes/intent-check.ts +++ b/packages/compiler/src/passes/intent-check.ts @@ -158,7 +158,7 @@ function findFirstCapabilityUse( (g) => g !== fn && g.tokenStart >= fn.tokenStart && g.tokenEnd <= fn.tokenEnd, ); - for (let i = fn.tokenStart; i < fn.tokenEnd; i++) { + for (let i = fn.bodyTokenStart ?? fn.tokenStart; i < fn.tokenEnd; i++) { if (insideAny(i, inner)) continue; const tok = tokens[i]; if (!tok || tok.kind !== "ident") continue; diff --git a/packages/compiler/tests/cap-check.test.ts b/packages/compiler/tests/cap-check.test.ts index 34f9f1e..4607643 100644 --- a/packages/compiler/tests/cap-check.test.ts +++ b/packages/compiler/tests/cap-check.test.ts @@ -307,3 +307,31 @@ describe("static capability check (0.2)", () => { } }); }); + +// --------------------------------------------------------------------------- +// Regression: stdlib namespace in parameter type annotation must not fire +// --------------------------------------------------------------------------- + +describe("cap-check: no false positive for stdlib namespace in parameter type", () => { + it("does not fire CAP001 when stdlib namespace appears in parameter type annotation", () => { + // `http.Client` is a type annotation, not a capability call. Scanning from + // fn.tokenStart used to flag this as http.x and fire CAP001. + const src = "?bs 0.9\nfn handleReq(client: http.Client) -> string = \"ok\"\n"; + expect(() => t(src)).not.toThrow(); + }); + + it("does not fire CAP001 when stdlib namespace appears in return type annotation", () => { + const src = "?bs 0.9\nfn makeClient() -> http.Client = http.newClient()\n"; + // Body calls http.newClient() without uses {net} — should fire for the body call but + // NOT double-fire for the return type annotation. + try { t(src); } catch { /* expected */ } + // Point: the test only checks the body-scan starts at body, not param/return type. + // The actual CAP001 fire is acceptable — we just don't want double diagnostics. + // This mainly documents the fix; the first test above is the regression sentinel. + }); + + it("still fires CAP001 when stdlib call is in the body (not the header)", () => { + const src = "?bs 0.9\nfn fetchData(url: string) -> string {\n http.get(url)\n}\n"; + expect(() => t(src)).toThrow(/CAP001/); + }); +}); From bfc28023e8f731c38f9ddd5fceb6cb5d878baa6b Mon Sep 17 00:00:00 2001 From: Marcelo Farias Date: Wed, 20 May 2026 14:40:26 -0300 Subject: [PATCH 2/2] fix(cap-check): strengthen return-type annotation regression test Replace the assertion-free second test case with one that actually verifies no CAP001 fires when the stdlib namespace appears only in the return type (body is a plain literal, not a stdlib call). Also removes the misleading "double-fire" comment: cap-check short-circuits on first error so double-fire is impossible; the real concern is false positives from type annotations. Co-Authored-By: Claude Sonnet 4.6 --- packages/compiler/tests/cap-check.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/compiler/tests/cap-check.test.ts b/packages/compiler/tests/cap-check.test.ts index 4607643..d246a2e 100644 --- a/packages/compiler/tests/cap-check.test.ts +++ b/packages/compiler/tests/cap-check.test.ts @@ -320,14 +320,11 @@ describe("cap-check: no false positive for stdlib namespace in parameter type", expect(() => t(src)).not.toThrow(); }); - it("does not fire CAP001 when stdlib namespace appears in return type annotation", () => { - const src = "?bs 0.9\nfn makeClient() -> http.Client = http.newClient()\n"; - // Body calls http.newClient() without uses {net} — should fire for the body call but - // NOT double-fire for the return type annotation. - try { t(src); } catch { /* expected */ } - // Point: the test only checks the body-scan starts at body, not param/return type. - // The actual CAP001 fire is acceptable — we just don't want double diagnostics. - // This mainly documents the fix; the first test above is the regression sentinel. + it("does not fire CAP001 when stdlib namespace appears only in return type annotation", () => { + // Return type `http.Client` is a type annotation, not a capability call. + // No stdlib call in the body — should compile clean. + const src = "?bs 0.9\nfn makeClient() -> http.Client = \"placeholder\"\n"; + expect(() => t(src)).not.toThrow(); }); it("still fires CAP001 when stdlib call is in the body (not the header)", () => {