Place a quoted name field among an error's fields

The position of an Error subclass's own `name` among its fields was found
from identifier keys only, so a quoted `"name"` field, other quoted
fields, and `this["name"] = ...` in a constructor were not seen, and
`name` was listed last. Field keys and constructor assignments now read
any statically spelled key: an identifier, a quoted string, a number in
its canonical spelling, or a computed string literal.
This commit is contained in:
Chris Tate
2026-10-20 02:07:18 -05:00
parent 619c857fcd
commit 9febfe8ca3
3 changed files with 38 additions and 10 deletions
@@ -6,17 +6,27 @@ type ErrorClassLink = Pick<ClassInfo, "decl" | "ctor" | "paramProps"> & {
base: ErrorClassLink | null;
};
/** Whether `node` is an assignment to `this.<name>`, and the name. */
/** The property key a member or element-access name spells statically:
* an identifier, a quoted string, a number in its canonical spelling, or a
* computed string literal. Null for other keys. */
function staticKeyText(name: ts.Node): string | null {
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
if (ts.isNumericLiteral(name)) return String(Number(name.text));
if (ts.isComputedPropertyName(name)) return staticKeyText(name.expression);
return null;
}
/** Whether `node` is an assignment to `this.<name>` or `this["<name>"]`,
* and the name. */
function thisAssignmentName(node: ts.Node): string | null {
if (!ts.isBinaryExpression(node) || node.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
return null;
const target = node.left;
if (
!ts.isPropertyAccessExpression(target) ||
target.expression.kind !== ts.SyntaxKind.ThisKeyword
)
return null;
return ts.isIdentifier(target.name) ? target.name.text : null;
if (ts.isPropertyAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword)
return ts.isIdentifier(target.name) ? target.name.text : null;
if (ts.isElementAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword)
return staticKeyText(target.argumentExpression);
return null;
}
/** Where an Error subclass instance's own `name` sits among its fields when
@@ -45,15 +55,17 @@ export function errorNameAfterField(
for (const link of chain) {
for (const property of link.paramProps ?? []) create(property.name);
for (const member of link.decl!.members.nodes) {
if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name)) continue;
if (!ts.isPropertyDeclaration(member)) continue;
const key = staticKeyText(member.name);
if (key === null) continue;
if (
member.modifiers?.nodes.some(
(m) => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.DeclareKeyword,
)
)
continue;
if (member.name.text === "name") return last;
create(member.name.text);
if (key === "name") return last;
create(key);
}
const body = link.ctor?.body;
if (!body) continue;
@@ -57,6 +57,11 @@ class NamedChild extends FieldBase {
}
}
class QuotedName extends Error {
override "name" = "QuotedName";
"quoted-extra" = 1;
}
class Unnamed extends Error {
extra = 1;
}
@@ -83,6 +88,7 @@ show("later field", new LaterNameField("c"));
show("code then name", new CodeThenName("d"));
show("child of named", new ChildOfNamed("e"));
show("named child", new NamedChild("f"));
show("quoted name field", new QuotedName("q"));
const late = new Unnamed("g");
late.name = "Late";
@@ -49,6 +49,15 @@ class Derived extends Base {
}
}
class ElementName extends Error {
constructor(message) {
super(message);
this.first = 1;
this["name"] = "ElementName";
this.second = 2;
}
}
class Unnamed extends Error {
constructor(message) {
super(message);
@@ -73,6 +82,7 @@ show("first", new AssignedFirst("b"));
show("declared field", new DeclaredField("c"));
show("name field", new NameField("d"));
show("derived", new Derived("e"));
show("element name", new ElementName("g"));
const late = new Unnamed("f");
late.name = "Late";