From d622fa2de7d105b6e4a575c48e845ea60d3c3cb8 Mon Sep 17 00:00:00 2001 From: Christian Schneider Date: Thu, 5 Dec 2024 19:06:14 +0100 Subject: [PATCH] langium: follow-up on #972 contributing convenience type defs for cross-ref properties distinction * fixed a nasty bug in internal type 'ExtractKeysOfValueType' by adding '-?' to the result object key list * improved documentation * contributed a bunch of tests in 'syntax-tree.test.ts' which is effectively tested during the TypeScript compilation: test failures occur as compilation errors --- packages/langium/src/syntax-tree.ts | 29 ++- packages/langium/test/syntax-tree.test.ts | 260 ++++++++++++++++++++++ 2 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 packages/langium/test/syntax-tree.test.ts diff --git a/packages/langium/src/syntax-tree.ts b/packages/langium/src/syntax-tree.ts index a318cf57a..47a548715 100644 --- a/packages/langium/src/syntax-tree.ts +++ b/packages/langium/src/syntax-tree.ts @@ -278,19 +278,22 @@ export function isRootCstNode(node: unknown): node is RootCstNode { } /** - * Returns a type to have only properties names (!) of a type T whose property value is of a certain type K. + * Describes a union type including only names(!) of properties of type T whose property value is of a certain type K, + * or 'never' in case of no such properties. + * It evaluates the value type regardless of the property being optional or not by converting T to Required. + * Note the '-?' in '[I in keyof T]-?:' that is required to map all optional but un-intended properties to 'never'. + * Without that, optional props like those inherited from 'AstNode' would be mapped to 'never|undefined', + * and the subsequent value mapping ('...[keyof T]') would yield 'undefined' instead of 'never' for AstNode types + * not having any property matching type K, which in turn yields follow-up errors. */ -type ExtractKeysOfValueType = { [I in keyof T]: T[I] extends K ? I : never }[keyof T]; +type ExtractKeysOfValueType = { [I in keyof T]-?: Required[I] extends K ? I : never }[keyof T]; /** - * Returns the property names (!) of an AstNode that are cross-references. - * Meant to be used during cross-reference resolution in combination with `assertUnreachable(context.property)`. + * Describes a union type including only names(!) of the cross-reference properties of the given AstNode type. + * Enhances compile-time validation of cross-reference distinctions, e.g. in scope providers + * in combination with `assertUnreachable(context.property)`. */ -export type CrossReferencesOfAstNodeType = ( - ExtractKeysOfValueType - | ExtractKeysOfValueType|undefined> -// eslint-disable-next-line @typescript-eslint/ban-types -) & {}; +export type CrossReferencesOfAstNodeType = ExtractKeysOfValueType; /** * Represents the enumeration-like type, that lists all AstNode types of your grammar. @@ -298,11 +301,13 @@ export type CrossReferencesOfAstNodeType = ( export type AstTypeList = Record; /** - * Returns all types that contain cross-references, A is meant to be the interface `XXXAstType` fromm your generated `ast.ts` file. - * Meant to be used during cross-reference resolution in combination with `assertUnreachable(context.container)`. + * Describes a union type including of all AstNode types containing cross-references. + * A is meant to be the interface `XXXAstType` fromm your generated `ast.ts` file. + * Enhances compile-time validation of cross-reference distinctions, e.g. in scope providers + * in combination with `assertUnreachable(context.container)`. */ export type AstNodeTypesWithCrossReferences> = { - [T in keyof A]: CrossReferencesOfAstNodeType extends never ? never : A[T] + [T in keyof A]-?: CrossReferencesOfAstNodeType extends never ? never : A[T] }[keyof A]; export type Mutable = { diff --git a/packages/langium/test/syntax-tree.test.ts b/packages/langium/test/syntax-tree.test.ts new file mode 100644 index 000000000..a218a465b --- /dev/null +++ b/packages/langium/test/syntax-tree.test.ts @@ -0,0 +1,260 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import { describe, test } from 'vitest'; +import type { AstNode, AstNodeTypesWithCrossReferences, AstTypeList, CrossReferencesOfAstNodeType, Reference } from '../src/syntax-tree.js'; + +describe('Utility types revealing cross-reference properties', () => { + + /** + * The tests listed below don't check correctness of 'CrossReferencesOfAstNodeType' + * and 'AstNodeTypesWithCrossReferences' by executing code and comparing results. + * Instead, they check if the types are correctly inferred by TypeScript. + * + * Hence, test failure are indicated by TypeScript errors, while the absence of any compile errors denotes success. + * + * Note: a value of type 'never' can be assigned to any other type, but not vice versa. + * In order to make sure 'never' _is not_ derived where they shouldn't be, esp. for the 'props' variables, 'props.length' is used. + * In order to make sure 'never' _is_ derived where it should be, the function 'checkNever' defined below is used. + */ + + // below follow some type definitions as produced by the Langium generator + + interface NoRefs extends AstNode { + $type: 'NoRefs'; + name: string; + } + + interface SingleRef extends AstNode { + $type: 'SingleRef'; + singleRef: Reference; + } + + interface OptionalSingleRef extends AstNode { + $type: 'OptionalSingleRef'; + optionalSingleRef?: Reference; + } + + interface MultiRef extends AstNode { + $type: 'MultiRef'; + multiRef: Array>; + } + + interface OptionalMultiRef extends AstNode { + $type: 'OptionalMultiRef'; + optionalMultiRef?: Array>; + } + + type PlainAstTypes = { + NoRefs: NoRefs, + SingleRef: SingleRef, + OptionalSingleRef: OptionalSingleRef, + MultiRef: MultiRef, + OptionalMultiRef: OptionalMultiRef + } + + const getAnyInstanceOfType = >() => >{}; + const getCrossRefProps = (_type: T) => >''; + const checkNever = (props: never) => props; + + test('Should not reveal cross-ref properties for NoRefs', () => { + const props = getCrossRefProps({}); + checkNever(props); + }); + + test('Should reveal cross-ref properties for SingleRef', () => { + const props = getCrossRefProps({}); + switch (props) { + case 'singleRef': + return props.length; + default: + return checkNever(props); + } + }); + + test('Should reveal cross-ref properties for OptionalSingleRef', () => { + const props = getCrossRefProps({}); + switch (props) { + case 'optionalSingleRef': + return props.length; + default: + return checkNever(props); + } + }); + + test('Should reveal cross-ref properties for MultiRef', () => { + const props = getCrossRefProps({}); + switch (props) { + case 'multiRef': + return props.length; + default: + return checkNever(props); + } + }); + + test('Should reveal cross-ref properties for OptionalMultiRef', () => { + const props = getCrossRefProps({}); + switch (props) { + case 'optionalMultiRef': + return props.length; + default: + return checkNever(props); + } + }); + + test('Should reveal AST Types with cross-references', () => { + const instance = getAnyInstanceOfType(); + switch (instance.$type) { + case 'SingleRef': + case 'OptionalSingleRef': + case 'MultiRef': + case 'OptionalMultiRef': + return instance.$type; + default: + return checkNever(instance); + } + }); + + test('Should reveal AST Types and their cross-references', () => { + const instance = getAnyInstanceOfType(); + switch (instance.$type) { + case 'SingleRef': { + const props = getCrossRefProps(instance); + switch (props) { + case 'singleRef': + return props.length; + default: + return checkNever(props); + } + } + case 'OptionalSingleRef': { + const props = getCrossRefProps(instance); + switch (props) { + case 'optionalSingleRef': + return props.length; + default: + return checkNever(props); + } + } + case 'MultiRef': { + const props = getCrossRefProps(instance); + switch (props) { + case 'multiRef': + return props.length; + default: + return checkNever(props); + } + } + case 'OptionalMultiRef': { + const props = getCrossRefProps(instance); + switch (props) { + case 'optionalMultiRef': + return props.length; + default: + return checkNever(props); + } + } + default: { + checkNever(instance); + return checkNever(getCrossRefProps(instance)); + } + } + }); + + interface SuperType extends AstNode { + readonly $type: 'SingleRefEx' | 'OptionalSingleRefEx' + } + + interface SingleRefEx extends SuperType { + readonly $type: 'SingleRefEx'; + singleRefEx: Reference; + } + + interface OptionalSingleRefEx extends SuperType { + readonly $type: 'OptionalSingleRefEx'; + optionalSingleRefEx?: Reference; + } + + interface SuperTypeIncludingNoRef extends AstNode { + readonly $type: 'NoRefsEx' | 'MultiRefEx' | 'OptionalMultiRefEx'; + } + + interface NoRefsEx extends SuperTypeIncludingNoRef { + readonly $type: 'NoRefsEx'; + name: string; + } + + interface MultiRefEx extends SuperTypeIncludingNoRef { + readonly $type: 'MultiRefEx'; + multiRefEx: Array>; + } + + interface OptionalMultiRefEx extends SuperTypeIncludingNoRef { + readonly $type: 'OptionalMultiRefEx'; + optionalMultiRefEx?: Array>; + } + + type TypesAndCommonSuperAstTypes = { + SuperType: SuperType, + SingleRefEx: SingleRefEx, + OptionalSingleRefEx: OptionalSingleRefEx, + + SuperTypeIncludingNoRef: SuperTypeIncludingNoRef + NoRefsEx: NoRefsEx, + MultiRefEx: MultiRefEx, + OptionalMultiRefEx: OptionalMultiRefEx, + } + + test('Should reveal AST Types inheriting common super types and their cross-references.', () => { + const instance = getAnyInstanceOfType(); + switch (instance.$type) { + case 'SingleRefEx': { + const props = getCrossRefProps(instance); + switch (props) { + case 'singleRefEx': + return props.length; + default: { + return checkNever(props); + } + } + } + case 'OptionalSingleRefEx': { + const props = getCrossRefProps(instance); + switch (props) { + case 'optionalSingleRefEx': + return props.length; + default: { + return checkNever(props); + } + } + } + case 'MultiRefEx': { + const props = getCrossRefProps(instance); + switch (props) { + case 'multiRefEx': + return props.length; + default: { + return checkNever(props); + } + } + } + case 'OptionalMultiRefEx': { + const props = getCrossRefProps(instance); + switch (props) { + case 'optionalMultiRefEx': + return props.length; + default: { + return checkNever(props); + } + } + } + default: { + checkNever(instance); + return checkNever(getCrossRefProps(instance)); + } + } + }); +}); \ No newline at end of file