Read TypeScript 7.1's heritage clause elements beside 7.0's (#2350)

This commit is contained in:
Chris Tate
2026-10-26 14:37:59 -05:00
committed by GitHub
5 changed files with 132 additions and 12 deletions
@@ -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(