mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Keep package results that pass their argument through out of the declaration boundary (#1964)
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