mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Keep stale narrowings in specialized standard-library arguments
A stale narrowing read in a call argument whose parameter accepts the declared type read as declared, so the callee's lowering tests the stored value. For standard-library callees that lowering is often specialized to the argument types programs pass: after `combo.p = {...}; combo()` the argument of `JSON.stringify(combo.p)` became `T | undefined` (SC1090), and after `ci(slow)` the handle of `timers.clearInterval(fast)` became `number | null` (SC2020), so 2072-expando-function-members.ts and 1804-timers-module.ts stopped compiling.
Call and construction arguments (directly or inside literal arguments) now read as declared only where the callee lowers any value of the parameter type: user callees, console methods, and standard-library parameters typed by a type parameter (`push(...items: T[])`, `Map.set`). Other standard-library arguments keep the checker's narrowing and the checked extraction.
This commit is contained in:
@@ -113,3 +113,43 @@ test("narrowed reads after code that can clear the field check it with Node's Ty
|
||||
expect(thrownMessages(module, "methodCall")).toContain(read);
|
||||
expect(thrownMessages(module, "loopBackEdge")).toContain(read);
|
||||
});
|
||||
|
||||
test("standard-library arguments read as declared only where the lowering is generic", async () => {
|
||||
// JSON.stringify and clearInterval lower only the narrowed argument types;
|
||||
// console methods and type-parameter slots take the declared value.
|
||||
const module = await lower(`
|
||||
interface Opts {
|
||||
p?: { [k: string]: number };
|
||||
}
|
||||
function touch(): number {
|
||||
return 1;
|
||||
}
|
||||
let handle: ReturnType<typeof setInterval> | null = null;
|
||||
const stop = (): void => {
|
||||
handle = null;
|
||||
};
|
||||
function specialized(o: Opts): string {
|
||||
o.p = { a: 1 };
|
||||
touch();
|
||||
return JSON.stringify(o.p);
|
||||
}
|
||||
function clearAfterCall(): void {
|
||||
if (handle !== null) {
|
||||
touch();
|
||||
clearInterval(handle);
|
||||
}
|
||||
}
|
||||
function formatted(o: Opts, out: (Opts["p"])[]): void {
|
||||
o.p = { a: 2 };
|
||||
touch();
|
||||
console.log(o.p);
|
||||
out.push(o.p);
|
||||
}
|
||||
handle = setInterval(() => {}, 1000);
|
||||
console.log(specialized({}));
|
||||
clearAfterCall();
|
||||
stop();
|
||||
formatted({}, []);
|
||||
`);
|
||||
expect(thrownMessages(module, "formatted")).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -141,6 +141,7 @@ export class NarrowingStability {
|
||||
const present = this.lowerer.mapTypeOf(checker.getNonNullableType(declared));
|
||||
if (present === null || !typeEquals(present, narrowedIr)) return null;
|
||||
} else if (position === "context") {
|
||||
if (!this.argumentSlotIsGeneric(node)) return null;
|
||||
const contextual = checker.getContextualType(node);
|
||||
if (contextual === undefined) return null;
|
||||
if (
|
||||
@@ -166,6 +167,48 @@ export class NarrowingStability {
|
||||
return this.trusted(node) ? null : declared;
|
||||
}
|
||||
|
||||
/** A read in a call or construction argument (directly or inside a literal
|
||||
* argument) reaches the callee's own lowering with the declared type. User
|
||||
* callees coerce any value of their parameter type, and so do the
|
||||
* standard library's generic slots: a parameter typed by a type parameter
|
||||
* (`push(...items: T[])`, `Map.set(key: K, value: V)`) and the console
|
||||
* methods, which format any value. Other standard-library lowerings are
|
||||
* specialized to the argument types programs pass and can refuse the
|
||||
* declared union (`JSON.stringify` of a possibly-undefined value,
|
||||
* `clearInterval` of a nullable handle). Those arguments keep the checker's
|
||||
* narrowing, and maybeNarrow reads them through the checked extraction. */
|
||||
private argumentSlotIsGeneric(node: ts.Expression): boolean {
|
||||
const slot = enclosingArgument(node);
|
||||
if (slot === null) return true;
|
||||
const { call, argument } = slot;
|
||||
const checker = this.lowerer.checker;
|
||||
const signature = checker.getResolvedSignature(call);
|
||||
const declaration = signature ? checker.signatureDeclaration(signature) : undefined;
|
||||
if (!declaration || !this.lowerer.isStdlibFile(declaration.getSourceFile())) return true;
|
||||
const callee = skipOuter(call.expression);
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
this.lowerer.isStdlibGlobal(skipOuter(callee.expression), "console")
|
||||
)
|
||||
return true;
|
||||
const index = call.arguments?.indexOf(argument) ?? -1;
|
||||
const params = declaration.parameters ?? [];
|
||||
if (index < 0 || params.length === 0) return false;
|
||||
const param = params[Math.min(index, params.length - 1)]!;
|
||||
if (index >= params.length && param.dotDotDotToken === undefined) return false;
|
||||
return param.type !== undefined && this.mentionsTypeParameter(param.type);
|
||||
}
|
||||
|
||||
private mentionsTypeParameter(node: ts.Node): boolean {
|
||||
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
|
||||
const symbol = this.lowerer.checker.getSymbolAtLocation(node.typeName);
|
||||
if (symbol && symbol.flags & ts.SymbolFlags.TypeParameter) return true;
|
||||
}
|
||||
return (
|
||||
ts.forEachChild(node, (child) => this.mentionsTypeParameter(child) || undefined) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
/** The type of the accessed property as declared on the receiver's
|
||||
* (instantiated) type, before any flow narrowing of this reference. */
|
||||
private declaredPropertyType(node: ts.PropertyAccessExpression): ts.Type | null {
|
||||
@@ -867,6 +910,28 @@ function widePosition(node: ts.Expression): WidePosition | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The call or construction whose argument holds `node`, directly or as an
|
||||
* element or property of literal arguments, with that argument. */
|
||||
function enclosingArgument(
|
||||
node: ts.Expression,
|
||||
): { call: ts.CallExpression | ts.NewExpression; argument: ts.Expression } | null {
|
||||
let child: ts.Node = node;
|
||||
let parent = node.parent;
|
||||
while (
|
||||
ts.isParenthesizedExpression(parent) ||
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
ts.isObjectLiteralExpression(parent) ||
|
||||
ts.isShorthandPropertyAssignment(parent) ||
|
||||
(ts.isPropertyAssignment(parent) && parent.initializer === child)
|
||||
) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!(ts.isCallExpression(parent) || ts.isNewExpression(parent))) return null;
|
||||
const argument = parent.arguments?.find((arg) => arg === child);
|
||||
return argument ? { call: parent, argument } : null;
|
||||
}
|
||||
|
||||
/** The array or object literal holding `parent` (an element's literal or a
|
||||
* property assignment) is itself lowered against a declared slot type. */
|
||||
function typedLiteral(
|
||||
|
||||
Reference in New Issue
Block a user