From 05dda22efa5ac3039211bedb9a3d7a41ccc27153 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 26 Oct 2026 14:09:17 -0500 Subject: [PATCH] Read TypeScript 7.1's heritage clause elements beside 7.0's TypeScript 7.1 parses the elements of an interface's `extends` and a class's `implements` as TypeReference nodes, whose names are entity names, where 7.0 gives ExpressionWithTypeArguments nodes. heritage-elements.ts recognizes both forms, and the readers that took an element's expression read either: - contract-types.ts finds `implements ProgramLifetime` and `implements Acyclic` written as type references; - narrowing-stability.ts takes a qualified name as it takes a property access; - generic-functions.ts reads an element's name in either form, and keeps counting a name in a heritage clause as a value reference. --- .../compiler/src/frontend/contract-types.ts | 15 ++-- .../src/frontend/heritage-elements.test.ts | 82 +++++++++++++++++++ .../src/frontend/heritage-elements.ts | 28 +++++++ .../frontend/lowering/generic-functions.ts | 13 ++- .../frontend/lowering/narrowing-stability.ts | 6 +- 5 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/src/frontend/heritage-elements.test.ts create mode 100644 packages/compiler/src/frontend/heritage-elements.ts diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts index 4ded2a1a0..a7bf32268 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -11,6 +11,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"; @@ -86,11 +87,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)) @@ -168,7 +169,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++; @@ -231,7 +232,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 { diff --git a/packages/compiler/src/frontend/heritage-elements.test.ts b/packages/compiler/src/frontend/heritage-elements.test.ts new file mode 100644 index 000000000..4ceb3c983 --- /dev/null +++ b/packages/compiler/src/frontend/heritage-elements.test.ts @@ -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); + } +}); diff --git a/packages/compiler/src/frontend/heritage-elements.ts b/packages/compiler/src/frontend/heritage-elements.ts new file mode 100644 index 000000000..b937c073a --- /dev/null +++ b/packages/compiler/src/frontend/heritage-elements.ts @@ -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; +} diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index dcd2efc08..046b34008 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index d0c9b04a6..9fae65a89 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -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(