Keep missing runtime-key entries undefined in nested module-scope patterns

- The indexed-read prepass now visits nested object patterns with the nested element's own type. A module-scope binding from a missing runtime-key entry of a nested index-signature record (`const { limits: { [key]: limit } } = config`) holds undefined instead of throwing a TypeError, with or without a default for the nested source.
- Corpus 6492 adds both nested forms.
This commit is contained in:
Chris Tate
2026-10-20 19:09:12 -05:00
parent 9baa93b799
commit 222ea51bd8
2 changed files with 26 additions and 4 deletions
@@ -5377,14 +5377,25 @@ export class Lowerer {
/** `const { a, b } = table` over an index-signature record: a name that
* is not a declared field reads the index signature and can be
* missing. */
const scanRecordPattern = (name: ts.ObjectBindingPattern, init: ts.Expression): boolean => {
const sourceType = this.mapTypeOf(this.typeOf(init));
const scanRecordPattern = (
name: ts.ObjectBindingPattern,
init: ts.Expression,
sourceType: IrType | null = this.mapTypeOf(this.typeOf(init)),
): boolean => {
if (sourceType?.kind !== "record") return false;
const shape = this.shapes.get(sourceType.shapeId);
const indexValue = shape?.indexValue;
if (!shape || shape.tuple || !indexValue || indexValue.kind === "dyn") return false;
if (!shape || shape.tuple) return false;
const indexValue = shape.indexValue;
let changed = false;
for (const el of name.elements.nodes) {
// A nested pattern reads its element the same way
// (`const { limits: { [key]: limit } } = config`).
if (!el.dotDotDotToken && el.name !== undefined && ts.isObjectBindingPattern(el.name)) {
if (scanRecordPattern(el.name, init, this.mapTypeOf(this.typeOf(el.name))))
changed = true;
continue;
}
if (!indexValue || indexValue.kind === "dyn") continue;
if (el.dotDotDotToken || el.initializer || !ts.isIdentifier(el.name)) continue;
const key = el.propertyName ?? el.name;
// A computed key that folds to one name reads like that name; a
@@ -51,3 +51,14 @@ for (const resource of ["cpu", "memory", "disk"]) {
const missingKey = process.argv.length > 99 ? "a" : "missing";
const { [missingKey]: missingWeight } = weights;
console.log(missingWeight, missingWeight === undefined);
// The same in a nested module-scope pattern, with and without a default
// for the nested source.
const nestedConfig = { limits: { cpu: 2 } as Record<string, number> };
const {
limits: { [missingKey]: nestedLimit },
} = nestedConfig;
const {
limits: { [missingKey]: defaultedLimit } = {},
} = nestedConfig;
console.log(nestedLimit, defaultedLimit === undefined);