mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep package results that pass their argument through out of the declaration boundary
A package function declared to return its own type parameter, which also types one of its parameters (<T extends object>(obj: T) => T), hands back the argument or something standing in for it, and the package may track that object's identity. Effect's Equal.byReference returns a proxy it keeps in a WeakSet. The declaration-typed boundary cast such a result to the argument's type and built a separate native record, so Equal.equals(a, b) on two by-reference objects compared them by value and printed true where Node prints false. The result's type comes from the caller's argument, not from data the package declares, so the projection no longer casts these calls. The package re-export coverage test now expects three dynamic statements: since the boundary, readRows's declared number result keeps its type, so count - 1 in 7004-package-declared-type-reexports is native arithmetic.
This commit is contained in:
@@ -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)) {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/decl-pass-through/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
+8
@@ -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;
|
||||
+18
@@ -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 };
|
||||
}
|
||||
+1
@@ -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" }
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user