mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Read TypeScript 7.1's heritage clause elements beside 7.0's (#2350)
This commit is contained in:
@@ -12,6 +12,7 @@ import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
import { locOf } from "./program.js";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { type HeritageElement, isHeritageElement } from "./heritage-elements.js";
|
||||
|
||||
export const CONTRACT_TYPES_MODULE = "@scriptc/types";
|
||||
|
||||
@@ -87,11 +88,11 @@ function markerOf(checker: ts.TypeChecker, ref: ts.TypeReferenceNode): ContractM
|
||||
}
|
||||
|
||||
/** The marker a heritage clause names (`implements Acyclic`,
|
||||
* `implements sc.ProgramLifetime`). */
|
||||
function markerOfHeritage(
|
||||
checker: ts.TypeChecker,
|
||||
node: ts.ExpressionWithTypeArguments,
|
||||
): ContractMarker | null {
|
||||
* `implements sc.ProgramLifetime`), as an ExpressionWithTypeArguments or,
|
||||
* in TypeScript 7.1's interface `extends` and class `implements`, a
|
||||
* TypeReference. */
|
||||
function markerOfHeritage(checker: ts.TypeChecker, node: HeritageElement): ContractMarker | null {
|
||||
if (ts.isTypeReferenceNode(node)) return markerOf(checker, node);
|
||||
const e = node.expression;
|
||||
if (ts.isIdentifier(e)) return markerNamed(checker, e, e.text, false);
|
||||
if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && ts.isIdentifier(e.name))
|
||||
@@ -169,7 +170,7 @@ export function collectContractAnnotations(
|
||||
ts.walkPreorder(
|
||||
sf,
|
||||
(node) => {
|
||||
if (ts.isExpressionWithTypeArguments(node)) {
|
||||
if (isHeritageElement(node)) {
|
||||
const marker = markerOfHeritage(checker, node);
|
||||
if (marker === null) return;
|
||||
out.count++;
|
||||
@@ -232,7 +233,7 @@ function unsupported(out: ContractAnnotations, node: ts.Node, marker: ContractMa
|
||||
function placeHeritage(
|
||||
out: ContractAnnotations,
|
||||
checker: ts.TypeChecker,
|
||||
node: ts.ExpressionWithTypeArguments,
|
||||
node: HeritageElement,
|
||||
marker: ContractMarker,
|
||||
resolveUnit: ContractUnitResolver,
|
||||
): void {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { collectContractAnnotations } from "./contract-types.js";
|
||||
import { isHeritageElement, heritageElementTarget } from "./heritage-elements.js";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { Ts7Api } from "./ts7/api.js";
|
||||
import { Ts7Host } from "./ts7/program-host.js";
|
||||
import { Ts7RpcClient } from "./ts7/rpc-client.js";
|
||||
import { TS7_FILE_SYSTEM_CALLBACKS } from "./ts7/rpc-filesystem.js";
|
||||
import { spawnTs7Wire } from "./ts7/rpc-process.js";
|
||||
import { ts7TestServers, type Ts7TestServer } from "./ts7/test-servers.js";
|
||||
|
||||
const corpus = fileURLToPath(new URL("../../../../tests/corpus/", import.meta.url));
|
||||
|
||||
/** Corpus programs whose classes implement contract markers and interfaces. */
|
||||
const programs = [
|
||||
"4636-contract-program-lifetime-references.ts",
|
||||
"5010-lifetime-region-generations.ts",
|
||||
"7401-program-lifetime-array-teardown.ts",
|
||||
];
|
||||
|
||||
function host(server: Ts7TestServer): Ts7Host {
|
||||
return new Ts7Host(
|
||||
(options) =>
|
||||
new Ts7Api(
|
||||
new Ts7RpcClient(
|
||||
spawnTs7Wire(server.executable, [
|
||||
"--api",
|
||||
"--cwd",
|
||||
options.cwd,
|
||||
`--callbacks=${TS7_FILE_SYSTEM_CALLBACKS}`,
|
||||
]),
|
||||
),
|
||||
options.fs,
|
||||
options.collectTiming,
|
||||
),
|
||||
{ cwd: dirname(corpus) },
|
||||
);
|
||||
}
|
||||
|
||||
/** Each heritage element's clause token, kind and the text it refers to,
|
||||
* and the program's contract annotations. */
|
||||
function facts(server: Ts7TestServer, file: string) {
|
||||
const programHost = host(server);
|
||||
try {
|
||||
const program = programHost.createProgram([corpus + file], {
|
||||
strict: true,
|
||||
types: [],
|
||||
target: ts.ScriptTarget.ESNext,
|
||||
});
|
||||
const source = program.getSourceFile(corpus + file)!;
|
||||
const elements: string[] = [];
|
||||
ts.walkPreorder(source, (node) => {
|
||||
if (!isHeritageElement(node)) return undefined;
|
||||
const clause = node.parent as ts.HeritageClause;
|
||||
elements.push(
|
||||
`${clause.token === ts.SyntaxKind.ImplementsKeyword ? "implements" : "extends"} ${ts.getTextOfNode(heritageElementTarget(node))}`,
|
||||
);
|
||||
return undefined;
|
||||
});
|
||||
const contracts = collectContractAnnotations(program.getTypeChecker(), [source]);
|
||||
return {
|
||||
elements,
|
||||
classes: [...contracts.classes.values()].map((note) => `${note.marker} ${note.name}`),
|
||||
acyclic: contracts.acyclic.map((entry) => entry.note.name),
|
||||
diagnostics: contracts.diagnostics.map((diagnostic) => diagnostic.code),
|
||||
};
|
||||
} finally {
|
||||
programHost.close();
|
||||
}
|
||||
}
|
||||
|
||||
test("heritage elements and the contracts they name read the same from tsc 7.0 and tsc 7.1", () => {
|
||||
const [old, next] = ts7TestServers();
|
||||
for (const file of programs) {
|
||||
const expected = facts(old!, file);
|
||||
expect(expected.classes.length, file).toBeGreaterThan(0);
|
||||
expect(expected.diagnostics, file).toEqual([]);
|
||||
expect(facts(next!, file), file).toEqual(expected);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
|
||||
/** An element of a heritage clause. TypeScript 7.0 parses every element as
|
||||
* an ExpressionWithTypeArguments. TypeScript 7.1 parses the elements of an
|
||||
* interface's `extends` and a class's `implements` as TypeReference nodes,
|
||||
* whose names are entity names (an Identifier or a QualifiedName); a class's
|
||||
* `extends` element stays an ExpressionWithTypeArguments. */
|
||||
export type HeritageElement = ts.ExpressionWithTypeArguments | ts.TypeReferenceNode;
|
||||
|
||||
/** Whether `node` is a TypeReference that is a heritage clause element. */
|
||||
export function isHeritageTypeReference(node: ts.Node): node is ts.TypeReferenceNode {
|
||||
return (
|
||||
ts.isTypeReferenceNode(node) && node.parent !== undefined && ts.isHeritageClause(node.parent)
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `node` is a heritage clause element in either form. */
|
||||
export function isHeritageElement(node: ts.Node): node is HeritageElement {
|
||||
return ts.isExpressionWithTypeArguments(node) || isHeritageTypeReference(node);
|
||||
}
|
||||
|
||||
/** The name or expression a heritage clause element refers to: its
|
||||
* expression (an ExpressionWithTypeArguments) or its type name (a
|
||||
* TypeReference). */
|
||||
export function heritageElementTarget(element: ts.Node): ts.Expression | ts.EntityName {
|
||||
if (ts.isTypeReferenceNode(element)) return element.typeName;
|
||||
return (element as ts.ExpressionWithTypeArguments).expression;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { staticNameBindings } from "./static-function-names.js";
|
||||
import { positionTag } from "./structural-positions.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { heritageElementTarget, isHeritageTypeReference } from "../heritage-elements.js";
|
||||
import { bodyReadsArguments } from "../arguments-usage.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
@@ -655,8 +656,9 @@ function extendsParameter(
|
||||
}
|
||||
for (const clause of decl.heritageClauses?.nodes ?? [])
|
||||
for (const base of clause.types.nodes) {
|
||||
const symbol = ts.isIdentifier(base.expression)
|
||||
? lowerer.checker.getSymbolAtLocation(base.expression)
|
||||
const target = heritageElementTarget(base);
|
||||
const symbol = ts.isIdentifier(target)
|
||||
? lowerer.checker.getSymbolAtLocation(target)
|
||||
: undefined;
|
||||
if (symbol !== undefined && params.has(symbol)) return true;
|
||||
}
|
||||
@@ -3036,7 +3038,12 @@ function bindingReferences(
|
||||
resolved = lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? resolved;
|
||||
if (resolved !== symbol) return false;
|
||||
for (let p: ts.Node | undefined = parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isTypeNode(p) && p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments) return false;
|
||||
if (
|
||||
ts.isTypeNode(p) &&
|
||||
p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments &&
|
||||
!isHeritageTypeReference(p)
|
||||
)
|
||||
return false;
|
||||
if (ts.isStatement(p) || ts.isFunctionLike(p)) break;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { heritageElementTarget } from "../heritage-elements.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { typeEquals } from "../../ir/ir.js";
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
@@ -674,8 +675,9 @@ export class NarrowingStability {
|
||||
if (node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.Decorator)) return true;
|
||||
for (const clause of node.heritageClauses?.nodes ?? []) {
|
||||
for (const heritage of clause.types.nodes) {
|
||||
const e = heritage.expression;
|
||||
if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return true;
|
||||
const e = heritageElementTarget(heritage);
|
||||
if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e) && !ts.isQualifiedName(e))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return node.members.nodes.some(
|
||||
|
||||
Reference in New Issue
Block a user