diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts index 181e27e34..38583025d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts @@ -15,6 +15,7 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { sideEffectFreeOptionValue } from "../surfaces.js"; +import { stripTypeWrappers } from "../typed-arguments.js"; import { type IrExpr, type IrStmt, STRING, URL_T } from "../../../ir/ir.js"; /** The fs functions whose path parameters accept a URL: the path @@ -142,8 +143,9 @@ function checkUrlPaths( for (let i = first + 1; i < args.length; i++) if (!sideEffectFreeOptionValue(args[i]!)) last = i; const staged: ts.Expression[] = []; for (let i = first + 1; i <= last; i++) { - let e = args[i]!; - while (ts.isParenthesizedExpression(e)) e = e.expression; + // Keyed without type-only wrappers (`satisfies`, `as`, `!`), so a + // lowering that reads through them finds the saved value. + const e = stripTypeWrappers(args[i]!); // Literals are read by their members, so a whole-value slot cannot // hold them. if (ts.isObjectLiteralExpression(e) || ts.isArrayLiteralExpression(e)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 80b2a5938..38d3ca8d3 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,4 @@ -import { onlyForwarded } from "./typed-arguments.js"; +import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, noteUntypedStreamFlow, @@ -12046,11 +12046,11 @@ export class Lowerer { return this.stagedArgument(expr) ?? lowerExpr(this, expr); } - /** The saved value of a staged fs argument (stagedArguments), or null for - * every other expression. */ + /** The saved value of a staged fs argument (stagedArguments, keyed + * without type-only wrappers), or null for every other expression. */ private stagedArgument(node: ts.Expression): IrExpr | null { if (this.stagedArguments.size === 0) return null; - while (ts.isParenthesizedExpression(node)) node = node.expression; + node = stripTypeWrappers(node); if (!this.stagedArguments.has(node)) return null; const slot = this.stagedArguments.get(node); if (!slot) diff --git a/tests/corpus/6931-fs-url-paths.ts b/tests/corpus/6931-fs-url-paths.ts index cf6889066..effe64207 100644 --- a/tests/corpus/6931-fs-url-paths.ts +++ b/tests/corpus/6931-fs-url-paths.ts @@ -103,6 +103,16 @@ console.log("uid", uid); uid = -1; attempt("chown order scheme", () => fs.chownSync(new URL("http://a/"), uid, bumpUid())); console.log("uid", uid); +// A later argument with effects inside a type-only wrapper runs once. +let modeCalls = 0; +function countedMode(): number { + modeCalls++; + return 0o600; +} +attempt("satisfies", () => fs.chmodSync(file, countedMode() satisfies number)); +attempt("as", () => fs.chmodSync(file, countedMode() as number)); +attempt("non-null", () => fs.chmodSync(file, countedMode()!)); +console.log("mode calls", modeCalls); fs.rmSync(dir, { recursive: true, force: true }); console.log("done", fs.existsSync(dir));