mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Keep optional-chain calls out of presence exits
A call whose checker type is never exits the statement only when the call is made. In an optional chain (`maybe?.fail()`, `this.helper?.fail()`, `fail?.()`), a missing receiver or callee ends the chain without making the call, and the checker still types the call never when the chain's base has a non-nullable declared type, as an array element read does. Such a statement completes, so a read after it is not proven present. exitsAbruptly now skips a call whose callee chain uses `?.`.
This commit is contained in:
@@ -143,3 +143,34 @@ test("a call that never returns exits like a throw", () => {
|
||||
// Without the checker's answer a call is an ordinary statement.
|
||||
expect(proofs(body)).toEqual([false, false, false]);
|
||||
});
|
||||
|
||||
test("a call in an optional chain does not exit, whatever its type", () => {
|
||||
// Every call named `fail` has type never; `?.` skips the call when its
|
||||
// receiver or callee is missing, so the statement completes.
|
||||
const neverReturns = (call: ts.CallExpression): boolean => {
|
||||
const callee = call.expression;
|
||||
const name = ts.isIdentifier(callee)
|
||||
? callee.text
|
||||
: ts.isPropertyAccessExpression(callee)
|
||||
? callee.name.text
|
||||
: "";
|
||||
return name === "fail";
|
||||
};
|
||||
const body = `function f(x: string, maybe: { fail(m: string): never }) {
|
||||
if (x === undefined) maybe?.fail("missing");
|
||||
use(x);
|
||||
}
|
||||
function g(x: string) {
|
||||
if (x === undefined) { this.helper?.fail("missing"); }
|
||||
use(x);
|
||||
}
|
||||
function h(x: string, fail: (m: string) => never) {
|
||||
if (x === undefined) fail?.("missing");
|
||||
use(x);
|
||||
}
|
||||
function i(x: string, maybe: { fail(m: string): never }) {
|
||||
if (x === undefined) maybe!.fail("missing");
|
||||
use(x);
|
||||
}`;
|
||||
expect(proofs(body, neverReturns)).toEqual([false, false, false, true]);
|
||||
});
|
||||
|
||||
@@ -292,6 +292,24 @@ function statementProvesPresence(statement: ts.Statement, bindings: PresenceBind
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether `call`, or a call or access its callee is made of, uses `?.`: a
|
||||
* missing receiver or callee then ends the chain without making the call,
|
||||
* whatever type the checker gives it. */
|
||||
function inOptionalChain(call: ts.CallExpression): boolean {
|
||||
let e: ts.Expression = call;
|
||||
for (;;) {
|
||||
if (
|
||||
ts.isCallExpression(e) ||
|
||||
ts.isPropertyAccessExpression(e) ||
|
||||
ts.isElementAccessExpression(e)
|
||||
) {
|
||||
if (e.questionDotToken !== undefined) return true;
|
||||
e = e.expression;
|
||||
} else if (ts.isNonNullExpression(e) || ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
else return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The statement never completes normally: its last reachable statement
|
||||
* returns, throws, breaks, continues, or calls a function that never
|
||||
* returns. */
|
||||
@@ -306,7 +324,7 @@ function exitsAbruptly(statement: ts.Statement, bindings: PresenceBindings): boo
|
||||
if (ts.isExpressionStatement(statement)) {
|
||||
let e = statement.expression;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return ts.isCallExpression(e) && bindings.neverReturns?.(e) === true;
|
||||
return ts.isCallExpression(e) && !inOptionalChain(e) && bindings.neverReturns?.(e) === true;
|
||||
}
|
||||
if (ts.isBlock(statement)) {
|
||||
// A labeled statement inside could be the target of an inner break,
|
||||
|
||||
@@ -44,3 +44,19 @@ try {
|
||||
} catch (error) {
|
||||
if (error instanceof Error) console.log(error.message);
|
||||
}
|
||||
|
||||
// Behind `?.`, a missing reporter skips the call, so the read after it can
|
||||
// still be undefined.
|
||||
class Reporter {
|
||||
fail(what: string): never {
|
||||
throw new Error(what);
|
||||
}
|
||||
}
|
||||
const reporters: Reporter[] = [];
|
||||
function maybeWord(index: number): string {
|
||||
const known = words[index];
|
||||
if (known === undefined) reporters[index]?.fail(`no word at ${index}`);
|
||||
return known;
|
||||
}
|
||||
const missing = maybeWord(7);
|
||||
console.log(maybeWord(1), missing === undefined, typeof missing);
|
||||
|
||||
Reference in New Issue
Block a user