Skip to content

Commit

Permalink
langium: follow-up on #972 contributing convenience type defs for cro…
Browse files Browse the repository at this point in the history
…ss-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
  • Loading branch information
sailingKieler committed Dec 12, 2024
1 parent 4e96814 commit 67c147e
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 12 deletions.
29 changes: 17 additions & 12 deletions packages/langium/src/syntax-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,31 +278,36 @@ 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<T>.
* 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<T, K> = { [I in keyof T]: T[I] extends K ? I : never }[keyof T];
type ExtractKeysOfValueType<T, K> = { [I in keyof T]-?: Required<T>[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<N extends AstNode> = (
ExtractKeysOfValueType<N, Reference|undefined>
| ExtractKeysOfValueType<N, Array<Reference|undefined>|undefined>
// eslint-disable-next-line @typescript-eslint/ban-types
) & {};
export type CrossReferencesOfAstNodeType<N extends AstNode> = ExtractKeysOfValueType<N, Reference | Reference[]>;

/**
* Represents the enumeration-like type, that lists all AstNode types of your grammar.
*/
export type AstTypeList<T> = Record<keyof T, AstNode>;

/**
* 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<A extends AstTypeList<A>> = {
[T in keyof A]: CrossReferencesOfAstNodeType<A[T]> extends never ? never : A[T]
[T in keyof A]-?: CrossReferencesOfAstNodeType<A[T]> extends never ? never : A[T]
}[keyof A];

export type Mutable<T> = {
Expand Down
258 changes: 258 additions & 0 deletions packages/langium/test/syntax-tree.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/******************************************************************************
* 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, expectTypeOf, 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.
*/

// 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<NoRefs>;
}

interface OptionalSingleRef extends AstNode {
$type: 'OptionalSingleRef';
optionalSingleRef?: Reference<NoRefs>;
}

interface MultiRef extends AstNode {
$type: 'MultiRef';
multiRef: Array<Reference<NoRefs>>;
}

interface OptionalMultiRef extends AstNode {
$type: 'OptionalMultiRef';
optionalMultiRef?: Array<Reference<NoRefs>>;
}

type PlainAstTypes = {
NoRefs: NoRefs,
SingleRef: SingleRef,
OptionalSingleRef: OptionalSingleRef,
MultiRef: MultiRef,
OptionalMultiRef: OptionalMultiRef
}

const getAnyInstanceOfType = <T extends AstTypeList<T>>() => <AstNodeTypesWithCrossReferences<T>>{};
const getCrossRefProps = <T extends AstNode>(_type: T) => <CrossReferencesOfAstNodeType<T>>'';

test('Should not reveal cross-ref properties for NoRefs', () => {
const props = getCrossRefProps(<NoRefs>{});
expectTypeOf(props).toBeNever();
});

test('Should reveal cross-ref properties for SingleRef', () => {
const props = getCrossRefProps(<SingleRef>{});
switch (props) {
case 'singleRef':
return expectTypeOf(props).toEqualTypeOf<'singleRef'>;
default:
return expectTypeOf(props).toBeNever();
}
});

test('Should reveal cross-ref properties for OptionalSingleRef', () => {
const props = getCrossRefProps(<OptionalSingleRef>{});
switch (props) {
case 'optionalSingleRef':
return expectTypeOf(props).toEqualTypeOf<'optionalSingleRef'>;
default:
return expectTypeOf(props).toBeNever();
}
});

test('Should reveal cross-ref properties for MultiRef', () => {
const props = getCrossRefProps(<MultiRef>{});
switch (props) {
case 'multiRef':
return expectTypeOf(props).toEqualTypeOf<'multiRef'>;
default:
return expectTypeOf(props).toBeNever();
}
});

test('Should reveal cross-ref properties for OptionalMultiRef', () => {
const props = getCrossRefProps(<OptionalMultiRef>{});
switch (props) {
case 'optionalMultiRef':
return expectTypeOf(props).toEqualTypeOf<'optionalMultiRef'>;
default:
return expectTypeOf(props).toBeNever();
}
});

test('Should reveal AST Types with cross-references', () => {
const instance = getAnyInstanceOfType<PlainAstTypes>();
switch (instance.$type) {
case 'SingleRef':
case 'OptionalSingleRef':
case 'MultiRef':
case 'OptionalMultiRef':
return instance.$type;
default:
return expectTypeOf(instance).toBeNever();
}
});

test('Should reveal AST Types and their cross-references', () => {
const instance = getAnyInstanceOfType<PlainAstTypes>();
switch (instance.$type) {
case 'SingleRef': {
const props = getCrossRefProps(instance);
switch (props) {
case 'singleRef':
return expectTypeOf(props).toEqualTypeOf<'singleRef'>;
default:
return expectTypeOf(props).toBeNever();
}
}
case 'OptionalSingleRef': {
const props = getCrossRefProps(instance);
switch (props) {
case 'optionalSingleRef':
return expectTypeOf(props).toEqualTypeOf<'optionalSingleRef'>;
default:
return expectTypeOf(props).toBeNever();
}
}
case 'MultiRef': {
const props = getCrossRefProps(instance);
switch (props) {
case 'multiRef':
return expectTypeOf(props).toEqualTypeOf<'multiRef'>;
default:
return expectTypeOf(props).toBeNever();
}
}
case 'OptionalMultiRef': {
const props = getCrossRefProps(instance);
switch (props) {
case 'optionalMultiRef':
return expectTypeOf(props).toEqualTypeOf<'optionalMultiRef'>;
default:
return expectTypeOf(props).toBeNever();
}
}
default: {
expectTypeOf(instance).toBeNever();
expectTypeOf(getCrossRefProps(instance)).toBeNever();
return;
}
}
});

interface SuperType extends AstNode {
readonly $type: 'SingleRefEx' | 'OptionalSingleRefEx'
}

interface SingleRefEx extends SuperType {
readonly $type: 'SingleRefEx';
singleRefEx: Reference<NoRefs>;
}

interface OptionalSingleRefEx extends SuperType {
readonly $type: 'OptionalSingleRefEx';
optionalSingleRefEx?: Reference<NoRefs>;
}

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<Reference<NoRefs>>;
}

interface OptionalMultiRefEx extends SuperTypeIncludingNoRef {
readonly $type: 'OptionalMultiRefEx';
optionalMultiRefEx?: Array<Reference<NoRefs>>;
}

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<TypesAndCommonSuperAstTypes>();
switch (instance.$type) {
case 'SingleRefEx': {
const props = getCrossRefProps(instance);
switch (props) {
case 'singleRefEx':
return expectTypeOf(props).toEqualTypeOf<'singleRefEx'>;
default: {
return expectTypeOf(props).toBeNever();
}
}
}
case 'OptionalSingleRefEx': {
const props = getCrossRefProps(instance);
switch (props) {
case 'optionalSingleRefEx':
return expectTypeOf(props).toEqualTypeOf<'optionalSingleRefEx'>;
default: {
return expectTypeOf(props).toBeNever();
}
}
}
case 'MultiRefEx': {
const props = getCrossRefProps(instance);
switch (props) {
case 'multiRefEx':
return expectTypeOf(props).toEqualTypeOf<'multiRefEx'>;
default: {
return expectTypeOf(props).toBeNever();
}
}
}
case 'OptionalMultiRefEx': {
const props = getCrossRefProps(instance);
switch (props) {
case 'optionalMultiRefEx':
return expectTypeOf(props).toEqualTypeOf<'optionalMultiRefEx'>;
default: {
return expectTypeOf(props).toBeNever();
}
}
}
default: {
expectTypeOf(instance).toBeNever();
expectTypeOf(getCrossRefProps(instance)).toBeNever();
return;
}
}
});
});

0 comments on commit 67c147e

Please sign in to comment.