diff --git a/packages/compiler/src/parser/parse-fn.ts b/packages/compiler/src/parser/parse-fn.ts index fbe7ff5..afbda91 100644 --- a/packages/compiler/src/parser/parse-fn.ts +++ b/packages/compiler/src/parser/parse-fn.ts @@ -102,6 +102,17 @@ export interface FnDecl { /** Source offset of the unsafe reason string token (UTF-16 code units, inclusive). Used to anchor UNS002 at the right location. */ unsafeReasonStart?: number; returnType: string; + /** + * Token-array index of the first body token (`{` for block bodies, `=` for + * expression bodies). Effect-check passes use this to scan only from the body + * start rather than from `tokenStart`, avoiding false matches on idents in the + * parameter list or return-type annotation. + * + * Optional so that code constructing `FnDecl` values outside the parser (e.g. + * tests, mocks) is not forced to populate this field. Consumers should fall + * back to `tokenStart` when absent: `fn.bodyTokenStart ?? fn.tokenStart`. + */ + bodyTokenStart?: number; /** Body is a brace block OR a single-expression form (= pure / io / arbitrary). */ body: FnBody; } @@ -489,6 +500,7 @@ export function parseFn( unsafeReason, unsafeReasonStart, returnType, + bodyTokenStart: typeEnd, body, }; } diff --git a/packages/compiler/src/passes/dep-check.ts b/packages/compiler/src/passes/dep-check.ts index 34ceb7b..38487db 100644 --- a/packages/compiler/src/passes/dep-check.ts +++ b/packages/compiler/src/passes/dep-check.ts @@ -219,7 +219,7 @@ function collectCallees( const open: FnDecl[] = []; let nextInner = 0; - for (let i = fn.tokenStart; i < fn.tokenEnd; i++) { + for (let i = fn.bodyTokenStart ?? fn.tokenStart; i < fn.tokenEnd; i++) { while (open.length > 0 && open[open.length - 1]!.tokenEnd <= i) open.pop(); while (nextInner < inner.length && inner[nextInner]!.tokenStart <= i) { open.push(inner[nextInner]!); diff --git a/packages/compiler/tests/dep-check.test.ts b/packages/compiler/tests/dep-check.test.ts index e706a85..688f2cf 100644 --- a/packages/compiler/tests/dep-check.test.ts +++ b/packages/compiler/tests/dep-check.test.ts @@ -191,3 +191,31 @@ describe("recursive fns", () => { expect(() => compile(src)).not.toThrow(); }); }); + +// --------------------------------------------------------------------------- +// Parameter-default false-positive regression (issue #70) +// collectCallees now starts from bodyTokenStart, skipping both the parameter +// list (including defaults) and the return-type annotation. The return-type +// exclusion is implicitly covered by the same mechanism — botscript return +// types don't support call-syntax idents, so no separate test is needed. +// --------------------------------------------------------------------------- + +describe("parameter-default exclusion (issue #70)", () => { + it("does not fire DEP001 when callee appears only in a parameter default, not the body", () => { + // `helper` is called in the parameter default of `caller` (evaluated at the + // call site), not in caller's body. collectCallees must not pick it up. + const src = + "?bs 0.9\n" + + "fn helper() reads { cache } -> string = \"x\"\n" + + "fn caller(x: string = helper()) -> string = x\n"; + expect(() => compile(src)).not.toThrow(); + }); + + it("still fires DEP001 when callee is called inside the body", () => { + const src = + "?bs 0.9\n" + + "fn helper() reads { cache } -> string = \"x\"\n" + + "fn caller() -> string { helper() }\n"; + expect(() => compile(src)).toThrow("DEP001"); + }); +});