mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep checked dictionary protocols and invalidate direct calls after method writes
This commit is contained in:
@@ -170,14 +170,19 @@ export function collectClassMethodMutations(
|
||||
// A string index whose values cannot be functions excludes callable
|
||||
// method slots. Class instances have no implicit index signature, so such
|
||||
// a dictionary is only a class view when the class declares that index
|
||||
// signature, and then every method must be assignable to its value type.
|
||||
const functionFreeDictionary = (value: ts.Expression): boolean =>
|
||||
lowerer.checker
|
||||
.getIndexInfosOfType(lowerer.typeOf(value))
|
||||
// signature, and then every method must be assignable to its value type,
|
||||
// or when the dictionary keeps checked identity: it then holds the
|
||||
// existing objects it receives, class instances included.
|
||||
const functionFreeDictionary = (value: ts.Expression): boolean => {
|
||||
const type = lowerer.typeOf(value);
|
||||
if (lowerer.classImplementedProtocol(type)) return false;
|
||||
return lowerer.checker
|
||||
.getIndexInfosOfType(type)
|
||||
.some(
|
||||
(index) =>
|
||||
(index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.type),
|
||||
);
|
||||
};
|
||||
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
|
||||
if (!ts.isIdentifier(key)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(key);
|
||||
|
||||
@@ -983,11 +983,13 @@ export class ClassProtocols {
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
const previous = this.answers.get(type);
|
||||
if (previous !== undefined) return previous;
|
||||
// A dictionary keeps checked identity when the protocols already mark
|
||||
// it (a literal whose methods read `this`) or when it receives existing
|
||||
// objects (markDictionaryViews).
|
||||
const dictionaryValue = this.dictionaryValueType(type);
|
||||
const answer =
|
||||
dictionaryValue !== undefined
|
||||
? (this.index(), this.dictionaryViews.has(dictionaryValue))
|
||||
: this.protocol(type);
|
||||
this.protocol(type) ||
|
||||
(dictionaryValue !== undefined && this.dictionaryViews.has(dictionaryValue));
|
||||
this.answers.set(type, answer);
|
||||
return answer;
|
||||
}
|
||||
|
||||
@@ -178,3 +178,20 @@ test("a store into a generic dictionary of dictionaries keeps checked dispatch",
|
||||
.replace("const settings: Settings =", "const settings: Settings<string> =");
|
||||
expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]);
|
||||
});
|
||||
|
||||
test("a runtime-key store into a dictionary view of a class instance keeps checked dispatch", async () => {
|
||||
const source = `
|
||||
class Counter {
|
||||
n = 0;
|
||||
tick(): number { return ++this.n; }
|
||||
}
|
||||
const counter = new Counter();
|
||||
const view: {} = counter;
|
||||
const slots: Record<string, string> = view;
|
||||
const key: string = String(process.argv.length);
|
||||
slots[key] = "replaced";
|
||||
console.log(counter.tick());
|
||||
`;
|
||||
// The method has a slot the write can replace, which calls consult.
|
||||
expect(await callersOf(source, "%Counter.tick")).toContain("%method.value:%Counter.tick");
|
||||
});
|
||||
|
||||
@@ -75,3 +75,15 @@ console.log(table["a"]);`),
|
||||
);
|
||||
expect(kinds["table"]).toBe("record");
|
||||
});
|
||||
|
||||
test("a dictionary literal whose methods read this keeps checked identity", async () => {
|
||||
const kinds = await globalKinds(
|
||||
program(`interface Table { [key: string]: () => number }
|
||||
const methods: Table = {
|
||||
base() { return 2; },
|
||||
twice() { return this.base() * 2; },
|
||||
};
|
||||
console.log(methods.twice());`),
|
||||
);
|
||||
expect(kinds["methods"]).toBe("dyn");
|
||||
});
|
||||
|
||||
@@ -53,9 +53,46 @@ const slots: Record<string, string> = greeterView;
|
||||
slots[process.argv.length > 99 ? "never" : "greet"] = "replaced";
|
||||
slots["name"] = "renamed";
|
||||
console.log(typeof greeter.greet, greeter.name, JSON.stringify(greeter));
|
||||
// A write under a runtime string key replaces a method too, and a direct
|
||||
// call reaches the replacement, which is not a function.
|
||||
class Counter {
|
||||
n = 0;
|
||||
tick(): number {
|
||||
return ++this.n;
|
||||
}
|
||||
}
|
||||
const counter = new Counter();
|
||||
const counterView: {} = counter;
|
||||
const counterSlots: Record<string, string> = counterView;
|
||||
const methodKey: string = process.argv.length > 99 ? "never" : "tick";
|
||||
counterSlots[methodKey] = "replaced";
|
||||
try {
|
||||
console.log(counter.tick());
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
try {
|
||||
console.log(greeter.greet());
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
|
||||
// Deleting through the dictionary removes the key from the object.
|
||||
const pruned: { a: string; b?: string } = { a: "x", b: "y" };
|
||||
const prunedTable: Record<string, string | undefined> = pruned;
|
||||
delete prunedTable[process.argv.length > 99 ? "never" : "b"];
|
||||
console.log(JSON.stringify(pruned), "b" in pruned);
|
||||
|
||||
// A dictionary literal whose methods read `this` keeps its live object.
|
||||
interface Table {
|
||||
[key: string]: () => number;
|
||||
}
|
||||
const table: Table = {
|
||||
base() {
|
||||
return 2;
|
||||
},
|
||||
twice() {
|
||||
return this.base() * 2;
|
||||
},
|
||||
};
|
||||
console.log(table.twice(), Object.keys(table).join(","));
|
||||
|
||||
Reference in New Issue
Block a user