Keep package results that pass their argument through out of the declaration boundary (#1964)

This commit is contained in:
Chris Tate
2026-10-23 02:30:41 -05:00
committed by GitHub
8 changed files with 127 additions and 12 deletions
@@ -99,6 +99,70 @@ function declaredInPackage(symbol: ts.Symbol | undefined): boolean {
return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d)));
}
/** The parts of a call signature's declaration that pass-through detection
* reads, for the declaration kinds a resolved call signature has. */
function signatureParts(node: ts.Node | undefined): {
typeParameters: readonly ts.TypeParameterDeclaration[];
parameters: readonly ts.ParameterDeclaration[];
type: ts.TypeNode | undefined;
} | null {
if (node === undefined) return null;
if (ts.isFunctionTypeNode(node))
return {
typeParameters: node.typeParameters?.nodes ?? [],
parameters: node.parameters.nodes,
type: node.type,
};
if (ts.isFunctionDeclaration(node))
return {
typeParameters: node.typeParameters?.nodes ?? [],
parameters: node.parameters.nodes,
type: node.type,
};
if (ts.isMethodDeclaration(node))
return {
typeParameters: node.typeParameters?.nodes ?? [],
parameters: node.parameters.nodes,
type: node.type,
};
if (ts.isMethodSignatureDeclaration(node))
return {
typeParameters: node.typeParameters?.nodes ?? [],
parameters: node.parameters.nodes,
type: node.type,
};
if (ts.isCallSignatureDeclaration(node))
return {
typeParameters: node.typeParameters?.nodes ?? [],
parameters: node.parameters.nodes,
type: node.type,
};
return null;
}
/** A signature that returns one of its own type parameters which also types
* a parameter (`<T extends object>(obj: T) => T`) hands back the argument
* or something standing in for it, a proxy or a marked object, whose
* identity the package may track (Effect's `Equal.byReference`). Its result
* type comes from the caller's argument, not from data the package
* declares, so the boundary leaves it alone: converting it would build a
* separate native value. */
function passesArgumentThrough(declaration: ts.Node | undefined): boolean {
const sig = signatureParts(declaration);
if (sig === null || sig.type === undefined || !ts.isTypeReferenceNode(sig.type)) return false;
const ret = sig.type;
if (!ts.isIdentifier(ret.typeName) || ret.typeArguments !== undefined) return false;
const name = ret.typeName.text;
if (!sig.typeParameters.some((tp) => tp.name.text === name)) return false;
return sig.parameters.some(
(param) =>
param.type !== undefined &&
ts.isTypeReferenceNode(param.type) &&
ts.isIdentifier(param.type.typeName) &&
param.type.typeName.text === name,
);
}
function declaredInUserFile(symbol: ts.Symbol | undefined): boolean {
if (symbol === undefined) return false;
const decls = symbol.declarations;
@@ -511,16 +575,16 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
if (ts.isCallExpression(d) || ts.isNewExpression(d)) {
const callee = d.expression;
const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
produced = declaredInPackage(declSymbol(target));
if (!produced) {
try {
const sig = declChecker.getResolvedSignature(d);
const decl = sig?.declaration;
produced = decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl));
} catch {
produced = false;
}
let decl: ts.Node | undefined;
try {
decl = declChecker.getResolvedSignature(d)?.declaration;
} catch {
decl = undefined;
}
produced =
(declaredInPackage(declSymbol(target)) ||
(decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)))) &&
!passesArgumentThrough(decl);
} else if (ts.isTaggedTemplateExpression(d)) {
produced = declaredInPackage(declSymbol(d.tag));
} else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/decl-pass-through/main.ts"
],
"diags": []
}
+16
View File
@@ -0,0 +1,16 @@
// Package functions declared to return their argument's own type
// (`<T extends object>(obj: T) => T`) hand back the argument, or a proxy
// standing in for it, and the package tracks that object's identity: those
// results stay the package's objects. Reads of data the declarations
// describe (`point(): Point`) are still typed.
import { byReference, equals, mark, point } from "ref-marks";
const a = byReference({ n: 42 });
const b = byReference({ n: 42 });
console.log(equals(a, a), equals(a, b), a.n + b.n);
const m = mark({ n: 1 });
const m2 = mark({ n: 1 });
console.log(equals(m, m), equals(m, m2));
const p = point(1, 2);
const q = point(1, 2);
console.log(equals(p, q), p.x + q.y);
@@ -0,0 +1,8 @@
export interface Point {
x: number;
y: number;
}
export declare function byReference<T extends object>(obj: T): T;
export declare const mark: <T extends object>(obj: T) => T;
export declare function equals(a: unknown, b: unknown): boolean;
export declare function point(x: number, y: number): Point;
@@ -0,0 +1,18 @@
const marked = new WeakSet();
export function byReference(obj) {
const proxy = new Proxy(obj, {});
marked.add(proxy);
return proxy;
}
export function mark(obj) {
marked.add(obj);
return obj;
}
export function equals(a, b) {
if (a === b) return true;
if (marked.has(a) || marked.has(b)) return false;
return JSON.stringify(a) === JSON.stringify(b);
}
export function point(x, y) {
return { x, y };
}
@@ -0,0 +1 @@
{ "name": "ref-marks", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" }
@@ -0,0 +1 @@
{ "name": "decl-pass-through-case", "private": true, "type": "module" }
+4 -3
View File
@@ -561,13 +561,14 @@ test("types only a package's .d.ts declares stay static through re-exports and .
// shape-kinds re-exports types its .d.ts entry imports; row-module's
// runtime entry is an .mjs file. Without their declared types the
// program's uses of Form, Kind and Row run in the dynamic tier (12
// statements); with them only the packages' own JavaScript and one
// arithmetic on a JavaScript return value do (4).
// statements); with them only the packages' own JavaScript does (3).
// readRows's result keeps its declared number type, so `count - 1` is
// native arithmetic too.
const { coverage } = analyze(
join(repoRoot, "tests/corpus/7004-package-declared-type-reexports/main.ts"),
);
expect(coverage.diagnostics).toEqual([]);
expect(coverage.stats.statementsDynamic).toBe(4);
expect(coverage.stats.statementsDynamic).toBe(3);
});
test("any-typed checked-dynamic operators compile natively", () => {