mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Return undefined from generic functions that read a missing element
A generic function, arrow or method whose body returns an array element or an index-signature entry now returns undefined for a missing one, as in Node, instead of throwing. Only declarations whose bodies return such a read change their instance result type. Fixes #964 Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
This commit is contained in:
@@ -157,6 +157,13 @@ export function declaredReturnType(
|
||||
}
|
||||
if (mappedReturn && isJsSourceFile(decl.getSourceFile()) && functionCanReturnUndefined(decl))
|
||||
mappedReturn = lowerer.withUndefinedArmOf(mappedReturn) ?? mappedReturn;
|
||||
// A generic-class method body that can return an indexed read that
|
||||
// misses returns undefined there, whatever the declared result says.
|
||||
if (mappedReturn && lowerer.runtimeOptionalGenericReturns.has(decl))
|
||||
mappedReturn =
|
||||
mappedReturn.kind === "promise"
|
||||
? { kind: "promise", inner: lowerer.runtimeOptionalType(mappedReturn.inner) }
|
||||
: lowerer.runtimeOptionalType(mappedReturn);
|
||||
if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "void") {
|
||||
let returnsValue = false;
|
||||
const scan = (node: ts.Node): void => {
|
||||
|
||||
@@ -292,11 +292,16 @@ export function genericCallInstance(
|
||||
(_param, i) => expr.arguments[i] ?? expr,
|
||||
);
|
||||
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
|
||||
const returnType = lowerer.mapTypeOf(retTs);
|
||||
if (!returnType) {
|
||||
const mappedReturn = lowerer.mapTypeOf(retTs);
|
||||
if (!mappedReturn) {
|
||||
fenceGenericSignatureResult(lowerer, expr, retTs);
|
||||
lowerer.badType(expr, retTs);
|
||||
}
|
||||
// A body that can return an indexed read that misses returns undefined
|
||||
// there, as in JavaScript, whatever the declared result says.
|
||||
const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl)
|
||||
? lowerer.runtimeOptionalType(mappedReturn)
|
||||
: mappedReturn;
|
||||
|
||||
// keyof-constrained type parameters (`K extends keyof T`): the bound
|
||||
// LITERAL is semantic — the instance body reads the named field — so
|
||||
|
||||
@@ -1880,6 +1880,10 @@ export class Lowerer {
|
||||
* declared optional binding to that read's bare type, so presence tests
|
||||
* on these bindings consult the stored tag instead. */
|
||||
readonly runtimeOptionalAssignedSymbols = new Set<ts.Symbol>();
|
||||
/** Generic function declarations whose body can return an indexed read
|
||||
* that misses.
|
||||
* Each instance's result admits undefined (generic-functions.ts). */
|
||||
readonly runtimeOptionalGenericReturns = new Set<ts.Node>();
|
||||
/** Concise callback/lambda returns promoted by the HOF callback prepass;
|
||||
* arrows have no declaration symbol to key in fnSigsBySymbol. */
|
||||
readonly runtimeOptionalFunctionReturns = new Map<ts.Node, IrType>();
|
||||
@@ -4289,6 +4293,79 @@ export class Lowerer {
|
||||
const read = this.mapTypeOf(this.typeOf(e));
|
||||
return !(read?.kind === "union" && this.armTag(read.unionId, UNDEFINED_T) >= 0);
|
||||
};
|
||||
/** An optional value in a generic body, where element types are type
|
||||
* parameters with no IR mapping yet: an element read of an array or a
|
||||
* read of a string index signature, judged on checker types. */
|
||||
const genericClassMethods: ts.MethodDeclaration[] = [];
|
||||
for (const sf of sourceFiles) {
|
||||
if (isJsSourceFile(sf) || sf.isDeclarationFile) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (!ts.isClassDeclaration(node)) return undefined;
|
||||
const genericClass = (node.typeParameters?.length ?? 0) > 0;
|
||||
for (const member of node.members)
|
||||
if (
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body &&
|
||||
(genericClass || (member.typeParameters?.length ?? 0) > 0)
|
||||
)
|
||||
genericClassMethods.push(member);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
const genericMayBeOptional = (node: ts.Expression): boolean => {
|
||||
if (explicitlyNonNull(node)) return false;
|
||||
if (mayBeOptional(node)) return true;
|
||||
const e = peel(node);
|
||||
if (ts.isConditionalExpression(e))
|
||||
return genericMayBeOptional(e.whenTrue) || genericMayBeOptional(e.whenFalse);
|
||||
if (ts.isBinaryExpression(e)) {
|
||||
const op = e.operatorToken.kind;
|
||||
if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken)
|
||||
return genericMayBeOptional(e.right);
|
||||
if (op === ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return genericMayBeOptional(e.left) || genericMayBeOptional(e.right);
|
||||
return false;
|
||||
}
|
||||
if (ts.isIdentifier(e)) {
|
||||
// A binding initialized from such a read and never reassigned.
|
||||
const symbol = symbolOf(e);
|
||||
const decl = symbol ? this.checker.valueDeclarationOf(symbol) : undefined;
|
||||
return (
|
||||
symbol !== null &&
|
||||
!reassigned.has(symbol) &&
|
||||
decl !== undefined &&
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer !== undefined &&
|
||||
ts.isIdentifier(decl.name) &&
|
||||
!provenPresent(e, symbol) &&
|
||||
genericIndexedRead(decl.initializer)
|
||||
);
|
||||
}
|
||||
return genericIndexedRead(e);
|
||||
};
|
||||
const genericIndexedRead = (node: ts.Expression): boolean => {
|
||||
const e = peel(node);
|
||||
if (!ts.isElementAccessExpression(e) || explicitlyNonNull(node)) return false;
|
||||
const read = typeOrNull(e);
|
||||
if (!read || (read.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Any)) !== 0) return false;
|
||||
if (
|
||||
read.isUnionType() &&
|
||||
ts.constituentTypes(read).some((t) => (t.flags & ts.TypeFlags.Undefined) !== 0)
|
||||
)
|
||||
return false;
|
||||
const receiver = typeOrNull(e.expression);
|
||||
if (!receiver || this.checker.isTupleType(receiver)) return false;
|
||||
if (this.checker.isArrayType(receiver)) return true;
|
||||
const key = e.argumentExpression;
|
||||
if (
|
||||
(ts.isStringLiteral(key) || ts.isNumericLiteral(key)) &&
|
||||
this.checker.getPropertyOfType(receiver, key.text) !== undefined
|
||||
)
|
||||
return false;
|
||||
return this.checker
|
||||
.getIndexInfosOfType(receiver)
|
||||
.some((info) => (info.keyType.flags & ts.TypeFlags.StringLike) !== 0);
|
||||
};
|
||||
const isDynamicObjectEntryRead = (node: ts.Expression): boolean => {
|
||||
const read = peel(node);
|
||||
if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false;
|
||||
@@ -5815,6 +5892,21 @@ export class Lowerer {
|
||||
for (const sf of sourceFiles) if (scanFile(sf)) changed = true;
|
||||
for (const [symbol, decl] of functionDeclBySymbol)
|
||||
if (scanReturns(symbol, decl)) changed = true;
|
||||
const noteGenericReturn = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): void => {
|
||||
if (optionalReturns.has(symbol) || isJsSourceFile(decl.getSourceFile())) return;
|
||||
if (!returnsOf(decl).some(genericMayBeOptional)) return;
|
||||
optionalReturns.add(symbol);
|
||||
this.runtimeOptionalGenericReturns.add(decl);
|
||||
provenance?.note("return", decl.name ?? decl, decl);
|
||||
changed = true;
|
||||
};
|
||||
for (const [symbol, info] of this.genericFnsBySymbol) noteGenericReturn(symbol, info.decl);
|
||||
// Generic methods and methods of a generic class collect with their
|
||||
// class, after this analysis; their result admits undefined there.
|
||||
for (const method of genericClassMethods) {
|
||||
const symbol = method.name ? symbolOf(method.name) : null;
|
||||
if (symbol) noteGenericReturn(symbol, method);
|
||||
}
|
||||
for (const fn of tsLambdas) {
|
||||
// An annotated return is the author's contract, as for parameters,
|
||||
// except that a var read before its first store is undefined
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5954-generic-optional-returns.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// A generic function, method or arrow that returns an element read or an
|
||||
// index-signature read answers undefined for a missing element or key, as
|
||||
// in Node, for every instantiation.
|
||||
export {};
|
||||
|
||||
function at<T>(items: T[], index: number): T {
|
||||
return items[index];
|
||||
}
|
||||
function lookup<T>(table: Record<string, T>, key: string): T {
|
||||
return table[key];
|
||||
}
|
||||
function viaBinding<T>(items: T[], index: number): T {
|
||||
const item = items[index];
|
||||
return item;
|
||||
}
|
||||
function either<T>(items: T[], index: number, fallback: T): T {
|
||||
return index >= 0 ? items[index] : fallback;
|
||||
}
|
||||
const first = <T,>(items: T[]): T => items[0];
|
||||
|
||||
console.log(at([1, 2], 1), at([1, 2], 5), at(["a"], 0), at(["a"], 3));
|
||||
console.log(lookup({ a: 1 }, "a"), lookup({ a: 1 }, "b"), lookup({ s: "x" }, "t"));
|
||||
console.log(viaBinding([true], 0), viaBinding([true], 2));
|
||||
console.log(either([1], 4, 9), either([1], -1, 9));
|
||||
console.log(first([7]), first<number>([]), first(["only"]));
|
||||
|
||||
const missing = at([10, 20], 7);
|
||||
console.log(missing === undefined, typeof missing);
|
||||
const label = lookup({ a: "A" }, "z");
|
||||
console.log(label ?? "none", label === undefined);
|
||||
|
||||
class Store<T> {
|
||||
items: Record<string, T>;
|
||||
constructor(items: Record<string, T>) {
|
||||
this.items = items;
|
||||
}
|
||||
get(key: string): T {
|
||||
return this.items[key];
|
||||
}
|
||||
pick<K extends string>(keys: K[], index: number): K {
|
||||
return keys[index];
|
||||
}
|
||||
}
|
||||
const store = new Store({ a: 1 });
|
||||
console.log(store.get("a"), store.get("b"), store.pick(["x", "y"], 1), store.pick(["x"], 4));
|
||||
|
||||
const pairs: [string, number][] = [["a", 1]];
|
||||
function second<T>(rows: T[][], row: number, column: number): T {
|
||||
return rows[row][column];
|
||||
}
|
||||
console.log(second([[1, 2]], 0, 1), second([[1, 2]], 0, 9), second(pairs, 0, 1));
|
||||
Reference in New Issue
Block a user