Count shorthand property reads of an initializer-less JavaScript let (#2252)

This commit is contained in:
Chris Tate
2026-10-25 15:54:08 -05:00
committed by GitHub
4 changed files with 56 additions and 6 deletions
@@ -5422,6 +5422,12 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
const scope = decl.parent?.parent?.parent;
if (symbol === undefined || (!ts.isBlock(scope) && !ts.isSourceFile(scope))) return null;
// A shorthand property's name (`{ width }`) resolves to the property
// symbol, but it reads the binding: the checker's shorthand-value symbol.
const refersToBinding = (node: ts.Identifier): boolean =>
(ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent)
: lowerer.checker.getSymbolAtLocation(node)) === symbol;
const isPlainAssignmentTarget = (node: ts.Identifier): boolean => {
let target: ts.Expression = node;
while (ts.isParenthesizedExpression(target.parent) && target.parent.expression === target) {
@@ -5437,11 +5443,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
const captures = (fn: ts.Node): boolean => {
let captured = false;
ts.walkPreorder(fn, (node) => {
if (
node !== fn &&
ts.isIdentifier(node) &&
lowerer.checker.getSymbolAtLocation(node) === symbol
) {
if (node !== fn && ts.isIdentifier(node) && refersToBinding(node)) {
captured = true;
return "stop";
}
@@ -5493,7 +5495,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
if (
ts.isIdentifier(node) &&
node !== decl.name &&
lowerer.checker.getSymbolAtLocation(node) === symbol &&
refersToBinding(node) &&
!isPlainAssignmentTarget(node)
) {
reads.push(node);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/evolving-let-shorthand-reads.js"
],
"diags": []
}
@@ -0,0 +1,41 @@
// An initializer-less JavaScript `let` read through a shorthand property
// (`{ width }`) still reads undefined on the paths that never assign it, as
// npm packages do when they build an options or size object.
function size(tty) {
let width;
if (tty) width = 80;
return { width };
}
/**
* @param {boolean} tty
* @returns {{ width: number | undefined }}
*/
function documented(tty) {
/** @type {number | undefined} */
let width;
if (tty) width = 80;
return { width };
}
function captured(tty) {
let width;
const read = () => ({ width });
const before = read().width;
if (tty) width = 80;
return [before, read().width];
}
function nested(flags) {
let columns;
let rows;
if (flags.columns) columns = 120;
if (flags.rows) rows = 40;
return { size: { columns, rows } };
}
console.log(size(true).width, size(false).width);
console.log(documented(true).width, documented(false).width);
console.log(JSON.stringify(size(false)), "width" in size(false));
console.log(captured(true), captured(false));
console.log(nested({ columns: true }), nested({ rows: true }), nested({}));
@@ -0,0 +1 @@
{ "plain": 3700, "san": 2800 }