From 77dc7a5ffdc6f29666ca7b01286e2e04ca0143e5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 28 Sep 2026 22:34:39 -0500 Subject: [PATCH] Dispatch literal bracket calls to native class methods --- .../src/frontend/lowering/lower-calls.ts | 23 +++++++++++++++++++ tests/corpus/class-bracket-method-calls.js | 15 ++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/corpus/class-bracket-method-calls.js diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 32a91ae35..1ab604360 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4897,6 +4897,29 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ts.isStringLiteralLike(expr.expression.argumentExpression) ) { const memberName = expr.expression.argumentExpression.text; + const receiverType = lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression)); + if (receiverType?.kind === "object") { + const info = lowerer.classes.get(receiverType.className); + const found = info ? lowerer.findMethodOn(info, memberName) : null; + if (info && found) { + const virtual = lowerer.overrideBelow(info, memberName); + if (found.sig.abstract === true && !virtual) { + lowerer.unsupported("SC1090", expr, `calls of the abstract method '${memberName}' with no concrete implementation below the receiver's static class`); + } + if (virtual) lowerer.noteVirtualEdge(info, memberName); + else lowerer.noteEdge(`%${found.declarer.def.name}.${memberName}`); + const receiver = lowerer.lowerExpr(expr.expression.expression); + const args = lowerer.completeArgs(expr.arguments, found.sig.params, locOf(expr), expr); + return reconcileOverloadReturn(lowerer, expr, { + ...(virtual + ? { kind: "virtualCall" as const, className: info.def.name, method: memberName } + : { kind: "call" as const, callee: `%${found.declarer.def.name}.${memberName}` }), + args: [lowerer.upcastTo(receiver, virtual ? info.def.name : found.declarer.def.name), ...args], + type: found.sig.ret, + loc: locOf(expr), + }); + } + } // ts7's getSymbolAtLocation does not resolve element accesses; the // member symbol comes from the receiver's (apparent) type instead — // same provenance answer as the dot spelling's name symbol. diff --git a/tests/corpus/class-bracket-method-calls.js b/tests/corpus/class-bracket-method-calls.js new file mode 100644 index 000000000..92233d175 --- /dev/null +++ b/tests/corpus/class-bracket-method-calls.js @@ -0,0 +1,15 @@ +class Base { + value() { return 'base'; } +} +class Derived extends Base { + value() { return 'derived'; } +} +const base = new Base(); +const derived = new Derived(); +console.log(base['value'](), derived['value']()); +let asBase = derived; +console.log(asBase['value']()); +class Keyword { + w\u0069th() { return 42; } +} +console.log(new Keyword()['with']());