Skip to content

Commit 138b1f9

Browse files
authored
fix(codegen): extend type resolution for call/method chains — fn()[i], obj.method()[i], and const x = obj.method().field now correctly propagate interface types to prevent garbage reads (#523)
Co-authored-by: cs01 <[email protected]>
1 parent ac14dec commit 138b1f9

4 files changed

Lines changed: 117 additions & 23 deletions

File tree

src/codegen/infrastructure/array-allocator.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
MemberAccessNode,
1111
VariableNode,
1212
MethodCallNode,
13+
CallNode,
1314
SourceLocation,
1415
} from "../../ast/types.js";
1516
import { InterfaceAllocator } from "./interface-allocator.js";
@@ -437,6 +438,26 @@ export class ArrayAllocator {
437438
return null;
438439
}
439440

441+
if (idxObjBase.type === "call") {
442+
const callExpr = indexExpr.object as CallNode;
443+
const ast = this.ctx.getAst();
444+
if (ast && callExpr.name) {
445+
const funcs = ast.functions || [];
446+
for (let i = 0; i < funcs.length; i++) {
447+
const fn = funcs[i];
448+
if (fn.name === callExpr.name && fn.returnType) {
449+
const rt = fn.returnType;
450+
if (rt.endsWith("[]")) {
451+
const elementType = rt.slice(0, -2).trim();
452+
return this.interfaceAlloc.getTypeInfoForElementType(elementType);
453+
}
454+
break;
455+
}
456+
}
457+
}
458+
return null;
459+
}
460+
440461
if (idxObjBase.type !== "member_access") return null;
441462

442463
const memberAccess = indexExpr.object as MemberAccessNode;

src/codegen/infrastructure/variable-allocator.ts

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export interface VariableAllocatorContext {
174174
setCurrentDeclaredInterfaceType(type: string | undefined): void;
175175
getCurrentDeclaredInterfaceType(): string | undefined;
176176
getCurrentClassName(): string | null;
177+
getMethodReturnType(className: string, methodName: string): string | null;
177178
getParameterTypeFromAST(paramName: string): string | null;
178179
resolveImportAlias(localName: string): string;
179180
typeResolverGetInterface(name: string): InterfaceDeclaration | null;
@@ -1223,33 +1224,70 @@ export class VariableAllocator {
12231224
if (exprBase.type !== "member_access") return null;
12241225
const memberExpr = expr as MemberAccessNode;
12251226
const objBase = memberExpr.object as ExprBase;
1226-
if (objBase.type !== "variable") return null;
1227-
const varName = (memberExpr.object as VariableNode).name;
1228-
if (!varName) return null;
12291227
let objectInterfaceType: string | null = null;
1230-
const ifaceType = this.ctx.symbolTable.getInterfaceType(varName);
1231-
if (ifaceType) {
1232-
objectInterfaceType = ifaceType;
1233-
} else {
1234-
const objMeta = this.ctx.symbolTable.getObjectMetadata(varName);
1235-
if (objMeta && objMeta.tsTypes) {
1236-
if (!objMeta.keys || !memberExpr.property) return null;
1237-
const keyIdx = objMeta.keys.indexOf(memberExpr.property);
1238-
if (keyIdx >= 0 && objMeta.tsTypes) {
1239-
const propType = objMeta.tsTypes[keyIdx];
1240-
if (
1241-
propType &&
1242-
!propType.endsWith("[]") &&
1243-
propType !== "string" &&
1244-
propType !== "number" &&
1245-
propType !== "boolean"
1246-
) {
1247-
const iface = this.getInterface(propType);
1248-
if (iface) return propType;
1228+
if (objBase.type === "method_call") {
1229+
const mc = memberExpr.object as MethodCallNode;
1230+
const mcObjBase = mc.object as ExprBase;
1231+
let mcClassName: string | null = null;
1232+
if (mcObjBase.type === "variable") {
1233+
const mcVar = mc.object as VariableNode;
1234+
const concrete = this.ctx.symbolTable.getConcreteClass(mcVar.name);
1235+
if (concrete) mcClassName = concrete;
1236+
else if (this.ctx.symbolTable.isClass(mcVar.name)) {
1237+
const ci = this.ctx.symbolTable.getClassInfo(mcVar.name);
1238+
if (ci) mcClassName = ci.className;
1239+
}
1240+
} else if (mcObjBase.type === "this") {
1241+
mcClassName = this.ctx.getCurrentClassName();
1242+
}
1243+
if (mcClassName) {
1244+
const rt = this.ctx.getMethodReturnType(mcClassName, mc.method);
1245+
if (rt && !rt.endsWith("[]")) {
1246+
objectInterfaceType = stripNullable(rt);
1247+
}
1248+
}
1249+
} else if (objBase.type === "call") {
1250+
const ce = memberExpr.object as CallNode;
1251+
const ast = this.ctx.getAst();
1252+
if (ast && ce.name) {
1253+
const funcs = ast.functions || [];
1254+
for (let i = 0; i < funcs.length; i++) {
1255+
const fn = funcs[i];
1256+
if (fn.name === ce.name && fn.returnType && !fn.returnType.endsWith("[]")) {
1257+
objectInterfaceType = stripNullable(fn.returnType);
1258+
break;
12491259
}
12501260
}
1251-
return null;
12521261
}
1262+
} else if (objBase.type === "variable") {
1263+
const varName = (memberExpr.object as VariableNode).name;
1264+
if (!varName) return null;
1265+
const ifaceType = this.ctx.symbolTable.getInterfaceType(varName);
1266+
if (ifaceType) {
1267+
objectInterfaceType = ifaceType;
1268+
} else {
1269+
const objMeta = this.ctx.symbolTable.getObjectMetadata(varName);
1270+
if (objMeta && objMeta.tsTypes) {
1271+
if (!objMeta.keys || !memberExpr.property) return null;
1272+
const keyIdx = objMeta.keys.indexOf(memberExpr.property);
1273+
if (keyIdx >= 0 && objMeta.tsTypes) {
1274+
const propType = objMeta.tsTypes[keyIdx];
1275+
if (
1276+
propType &&
1277+
!propType.endsWith("[]") &&
1278+
propType !== "string" &&
1279+
propType !== "number" &&
1280+
propType !== "boolean"
1281+
) {
1282+
const iface = this.getInterface(propType);
1283+
if (iface) return propType;
1284+
}
1285+
}
1286+
return null;
1287+
}
1288+
}
1289+
} else {
1290+
return null;
12531291
}
12541292
if (!objectInterfaceType) return null;
12551293
const objectInterface = this.getInterface(objectInterfaceType);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface P {
2+
x: number;
3+
y: string;
4+
}
5+
function make(): P[] {
6+
return [
7+
{ x: 1, y: "a" },
8+
{ x: 2, y: "b" },
9+
];
10+
}
11+
function main(): void {
12+
const last = make()[1];
13+
if (last.x === 2 && last.y === "b") console.log("TEST_PASSED");
14+
}
15+
main();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
interface Inner {
2+
v: number;
3+
}
4+
interface Outer {
5+
inner: Inner;
6+
name: string;
7+
}
8+
class S {
9+
build(): Outer {
10+
return { inner: { v: 9 }, name: "x" };
11+
}
12+
}
13+
function main(): void {
14+
const s = new S();
15+
const a = s.build().inner;
16+
if (a.v === 9) {
17+
console.log("TEST_PASSED");
18+
}
19+
}
20+
main();

0 commit comments

Comments
 (0)