Skip to content

Commit 3f9de4c

Browse files
langium: follow-up on #972 contributing convenience type defs for cross-ref property distinction (#1771)
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
1 parent e004619 commit 3f9de4c

File tree

2 files changed

+277
-12
lines changed

2 files changed

+277
-12
lines changed

packages/langium/src/syntax-tree.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -278,31 +278,36 @@ export function isRootCstNode(node: unknown): node is RootCstNode {
278278
}
279279

280280
/**
281-
* Returns a type to have only properties names (!) of a type T whose property value is of a certain type K.
281+
* Describes a union type including only names(!) of properties of type T whose property value is of a certain type K,
282+
* or 'never' in case of no such properties.
283+
* It evaluates the value type regardless of the property being optional or not by converting T to Required<T>.
284+
* Note the '-?' in '[I in keyof T]-?:' that is required to map all optional but un-intended properties to 'never'.
285+
* Without that, optional props like those inherited from 'AstNode' would be mapped to 'never|undefined',
286+
* and the subsequent value mapping ('...[keyof T]') would yield 'undefined' instead of 'never' for AstNode types
287+
* not having any property matching type K, which in turn yields follow-up errors.
282288
*/
283-
type ExtractKeysOfValueType<T, K> = { [I in keyof T]: T[I] extends K ? I : never }[keyof T];
289+
type ExtractKeysOfValueType<T, K> = { [I in keyof T]-?: Required<T>[I] extends K ? I : never }[keyof T];
284290

285291
/**
286-
* Returns the property names (!) of an AstNode that are cross-references.
287-
* Meant to be used during cross-reference resolution in combination with `assertUnreachable(context.property)`.
292+
* Describes a union type including only names(!) of the cross-reference properties of the given AstNode type.
293+
* Enhances compile-time validation of cross-reference distinctions, e.g. in scope providers
294+
* in combination with `assertUnreachable(context.property)`.
288295
*/
289-
export type CrossReferencesOfAstNodeType<N extends AstNode> = (
290-
ExtractKeysOfValueType<N, Reference|undefined>
291-
| ExtractKeysOfValueType<N, Array<Reference|undefined>|undefined>
292-
// eslint-disable-next-line @typescript-eslint/ban-types
293-
) & {};
296+
export type CrossReferencesOfAstNodeType<N extends AstNode> = ExtractKeysOfValueType<N, Reference | Reference[]>;
294297

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

300303
/**
301-
* Returns all types that contain cross-references, A is meant to be the interface `XXXAstType` fromm your generated `ast.ts` file.
302-
* Meant to be used during cross-reference resolution in combination with `assertUnreachable(context.container)`.
304+
* Describes a union type including of all AstNode types containing cross-references.
305+
* A is meant to be the interface `XXXAstType` fromm your generated `ast.ts` file.
306+
* Enhances compile-time validation of cross-reference distinctions, e.g. in scope providers
307+
* in combination with `assertUnreachable(context.container)`.
303308
*/
304309
export type AstNodeTypesWithCrossReferences<A extends AstTypeList<A>> = {
305-
[T in keyof A]: CrossReferencesOfAstNodeType<A[T]> extends never ? never : A[T]
310+
[T in keyof A]-?: CrossReferencesOfAstNodeType<A[T]> extends never ? never : A[T]
306311
}[keyof A];
307312

308313
export type Mutable<T> = {
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/******************************************************************************
2+
* Copyright 2024 TypeFox GmbH
3+
* This program and the accompanying materials are made available under the
4+
* terms of the MIT License, which is available in the project root.
5+
******************************************************************************/
6+
7+
import { describe, expectTypeOf, test } from 'vitest';
8+
import type { AstNode, AstNodeTypesWithCrossReferences, AstTypeList, CrossReferencesOfAstNodeType, Reference } from '../src/syntax-tree.js';
9+
10+
describe('Utility types revealing cross-reference properties', () => {
11+
12+
/**
13+
* The tests listed below don't check correctness of 'CrossReferencesOfAstNodeType'
14+
* and 'AstNodeTypesWithCrossReferences' by executing code and comparing results.
15+
* Instead, they check if the types are correctly inferred by TypeScript.
16+
*
17+
* Hence, test failure are indicated by TypeScript errors, while the absence of any compile errors denotes success.
18+
* Note: a value of type 'never' can be assigned to any other type, but not vice versa.
19+
*/
20+
21+
// below follow some type definitions as produced by the Langium generator
22+
23+
interface NoRefs extends AstNode {
24+
$type: 'NoRefs';
25+
name: string;
26+
}
27+
28+
interface SingleRef extends AstNode {
29+
$type: 'SingleRef';
30+
singleRef: Reference<NoRefs>;
31+
}
32+
33+
interface OptionalSingleRef extends AstNode {
34+
$type: 'OptionalSingleRef';
35+
optionalSingleRef?: Reference<NoRefs>;
36+
}
37+
38+
interface MultiRef extends AstNode {
39+
$type: 'MultiRef';
40+
multiRef: Array<Reference<NoRefs>>;
41+
}
42+
43+
interface OptionalMultiRef extends AstNode {
44+
$type: 'OptionalMultiRef';
45+
optionalMultiRef?: Array<Reference<NoRefs>>;
46+
}
47+
48+
type PlainAstTypes = {
49+
NoRefs: NoRefs,
50+
SingleRef: SingleRef,
51+
OptionalSingleRef: OptionalSingleRef,
52+
MultiRef: MultiRef,
53+
OptionalMultiRef: OptionalMultiRef
54+
}
55+
56+
const getAnyInstanceOfType = <T extends AstTypeList<T>>() => <AstNodeTypesWithCrossReferences<T>>{};
57+
const getCrossRefProps = <T extends AstNode>(_type: T) => <CrossReferencesOfAstNodeType<T>>'';
58+
59+
test('Should not reveal cross-ref properties for NoRefs', () => {
60+
const props = getCrossRefProps(<NoRefs>{});
61+
expectTypeOf(props).toBeNever();
62+
});
63+
64+
test('Should reveal cross-ref properties for SingleRef', () => {
65+
const props = getCrossRefProps(<SingleRef>{});
66+
switch (props) {
67+
case 'singleRef':
68+
return expectTypeOf(props).toEqualTypeOf<'singleRef'>;
69+
default:
70+
return expectTypeOf(props).toBeNever();
71+
}
72+
});
73+
74+
test('Should reveal cross-ref properties for OptionalSingleRef', () => {
75+
const props = getCrossRefProps(<OptionalSingleRef>{});
76+
switch (props) {
77+
case 'optionalSingleRef':
78+
return expectTypeOf(props).toEqualTypeOf<'optionalSingleRef'>;
79+
default:
80+
return expectTypeOf(props).toBeNever();
81+
}
82+
});
83+
84+
test('Should reveal cross-ref properties for MultiRef', () => {
85+
const props = getCrossRefProps(<MultiRef>{});
86+
switch (props) {
87+
case 'multiRef':
88+
return expectTypeOf(props).toEqualTypeOf<'multiRef'>;
89+
default:
90+
return expectTypeOf(props).toBeNever();
91+
}
92+
});
93+
94+
test('Should reveal cross-ref properties for OptionalMultiRef', () => {
95+
const props = getCrossRefProps(<OptionalMultiRef>{});
96+
switch (props) {
97+
case 'optionalMultiRef':
98+
return expectTypeOf(props).toEqualTypeOf<'optionalMultiRef'>;
99+
default:
100+
return expectTypeOf(props).toBeNever();
101+
}
102+
});
103+
104+
test('Should reveal AST Types with cross-references', () => {
105+
const instance = getAnyInstanceOfType<PlainAstTypes>();
106+
switch (instance.$type) {
107+
case 'SingleRef':
108+
case 'OptionalSingleRef':
109+
case 'MultiRef':
110+
case 'OptionalMultiRef':
111+
expectTypeOf(instance).toEqualTypeOf<SingleRef | OptionalSingleRef | MultiRef | OptionalMultiRef>();
112+
113+
return expectTypeOf(instance.$type).toEqualTypeOf<'SingleRef' | 'OptionalSingleRef' | 'MultiRef' | 'OptionalMultiRef'>();
114+
default:
115+
return expectTypeOf(instance).toBeNever();
116+
}
117+
});
118+
119+
test('Should reveal AST Types and their cross-references', () => {
120+
const instance = getAnyInstanceOfType<PlainAstTypes>();
121+
switch (instance.$type) {
122+
case 'SingleRef': {
123+
const props = getCrossRefProps(instance);
124+
switch (props) {
125+
case 'singleRef':
126+
return expectTypeOf(props).toEqualTypeOf<'singleRef'>;
127+
default:
128+
return expectTypeOf(props).toBeNever();
129+
}
130+
}
131+
case 'OptionalSingleRef': {
132+
const props = getCrossRefProps(instance);
133+
switch (props) {
134+
case 'optionalSingleRef':
135+
return expectTypeOf(props).toEqualTypeOf<'optionalSingleRef'>;
136+
default:
137+
return expectTypeOf(props).toBeNever();
138+
}
139+
}
140+
case 'MultiRef': {
141+
const props = getCrossRefProps(instance);
142+
switch (props) {
143+
case 'multiRef':
144+
return expectTypeOf(props).toEqualTypeOf<'multiRef'>;
145+
default:
146+
return expectTypeOf(props).toBeNever();
147+
}
148+
}
149+
case 'OptionalMultiRef': {
150+
const props = getCrossRefProps(instance);
151+
switch (props) {
152+
case 'optionalMultiRef':
153+
return expectTypeOf(props).toEqualTypeOf<'optionalMultiRef'>;
154+
default:
155+
return expectTypeOf(props).toBeNever();
156+
}
157+
}
158+
default: {
159+
expectTypeOf(instance).toBeNever();
160+
expectTypeOf(getCrossRefProps(instance)).toBeNever();
161+
return;
162+
}
163+
}
164+
});
165+
166+
interface SuperType extends AstNode {
167+
readonly $type: 'SingleRefEx' | 'OptionalSingleRefEx'
168+
}
169+
170+
interface SingleRefEx extends SuperType {
171+
readonly $type: 'SingleRefEx';
172+
singleRefEx: Reference<NoRefs>;
173+
}
174+
175+
interface OptionalSingleRefEx extends SuperType {
176+
readonly $type: 'OptionalSingleRefEx';
177+
optionalSingleRefEx?: Reference<NoRefs>;
178+
}
179+
180+
interface SuperTypeIncludingNoRef extends AstNode {
181+
readonly $type: 'NoRefsEx' | 'MultiRefEx' | 'OptionalMultiRefEx';
182+
}
183+
184+
interface NoRefsEx extends SuperTypeIncludingNoRef {
185+
readonly $type: 'NoRefsEx';
186+
name: string;
187+
}
188+
189+
interface MultiRefEx extends SuperTypeIncludingNoRef {
190+
readonly $type: 'MultiRefEx';
191+
multiRefEx: Array<Reference<NoRefs>>;
192+
}
193+
194+
interface OptionalMultiRefEx extends SuperTypeIncludingNoRef {
195+
readonly $type: 'OptionalMultiRefEx';
196+
optionalMultiRefEx?: Array<Reference<NoRefs>>;
197+
}
198+
199+
type TypesAndCommonSuperAstTypes = {
200+
SuperType: SuperType,
201+
SingleRefEx: SingleRefEx,
202+
OptionalSingleRefEx: OptionalSingleRefEx,
203+
204+
SuperTypeIncludingNoRef: SuperTypeIncludingNoRef
205+
NoRefsEx: NoRefsEx,
206+
MultiRefEx: MultiRefEx,
207+
OptionalMultiRefEx: OptionalMultiRefEx,
208+
}
209+
210+
test('Should reveal AST Types inheriting common super types and their cross-references.', () => {
211+
const instance = getAnyInstanceOfType<TypesAndCommonSuperAstTypes>();
212+
switch (instance.$type) {
213+
case 'SingleRefEx': {
214+
const props = getCrossRefProps(instance);
215+
switch (props) {
216+
case 'singleRefEx':
217+
return expectTypeOf(props).toEqualTypeOf<'singleRefEx'>;
218+
default: {
219+
return expectTypeOf(props).toBeNever();
220+
}
221+
}
222+
}
223+
case 'OptionalSingleRefEx': {
224+
const props = getCrossRefProps(instance);
225+
switch (props) {
226+
case 'optionalSingleRefEx':
227+
return expectTypeOf(props).toEqualTypeOf<'optionalSingleRefEx'>;
228+
default: {
229+
return expectTypeOf(props).toBeNever();
230+
}
231+
}
232+
}
233+
case 'MultiRefEx': {
234+
const props = getCrossRefProps(instance);
235+
switch (props) {
236+
case 'multiRefEx':
237+
return expectTypeOf(props).toEqualTypeOf<'multiRefEx'>;
238+
default: {
239+
return expectTypeOf(props).toBeNever();
240+
}
241+
}
242+
}
243+
case 'OptionalMultiRefEx': {
244+
const props = getCrossRefProps(instance);
245+
switch (props) {
246+
case 'optionalMultiRefEx':
247+
return expectTypeOf(props).toEqualTypeOf<'optionalMultiRefEx'>;
248+
default: {
249+
return expectTypeOf(props).toBeNever();
250+
}
251+
}
252+
}
253+
default: {
254+
expectTypeOf(instance).toBeNever();
255+
expectTypeOf(getCrossRefProps(instance)).toBeNever();
256+
return;
257+
}
258+
}
259+
});
260+
});

0 commit comments

Comments
 (0)