Skip to content

Commit 95fd0c2

Browse files
authored
add DeepPartialAstNode utility (#1875)
1 parent c5de9ba commit 95fd0c2

File tree

2 files changed

+84
-0
lines changed

2 files changed

+84
-0
lines changed

packages/langium/src/utils/ast-utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,30 @@ export function copyAstNode<T extends AstNode = AstNode>(node: T, buildReference
301301
linkContentToContainer(copy);
302302
return copy as unknown as T;
303303
}
304+
305+
/**
306+
* Recursively makes all properties of an AstNode optional, except for those
307+
* that start with a dollar sign ($) or are of type boolean or are of type array.
308+
* If the type is a Reference or an Array, it applies the transformation recursively
309+
* to the inner type.
310+
* Otherwise the type is returned as is.
311+
*
312+
* @template T - The type to be transformed.
313+
*/
314+
export type DeepPartialAstNode<T> =
315+
// if T is a Reference<U> transform it to Reference<DeepPartialAstNode<U>>
316+
T extends Reference<infer U extends AstNode> ? Reference<DeepPartialAstNode<U>> :
317+
// if T is an AstNode
318+
T extends AstNode ? {
319+
// transform the type of each property starting with '$' or with a boolean or array type
320+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
321+
[K in keyof T as K extends `$${string}` | (T[K] extends (boolean | any[]) ? K : never) ? K : never]: DeepPartialAstNode<T[K]>;
322+
} & {
323+
// force the property as optional and transform its type for each property not starting with '$' or with a type different from boolean or array type
324+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
325+
[K in keyof T as K extends `$${string}` ? never: T[K] extends (boolean | any[]) ? never : K]?: DeepPartialAstNode<T[K]>;
326+
} :
327+
// if T is an Array<U> convert to Array<DeepPartialAstNode<U>>
328+
T extends Array<infer U> ? Array<DeepPartialAstNode<U>> :
329+
// otherwise keep T as is
330+
T;

packages/langium/test/utils/ast-utils.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,61 @@
55
******************************************************************************/
66

77
import { describe, expect, test } from 'vitest';
8+
import type { AstNode, Reference } from 'langium';
89
import { AstUtils, EmptyFileSystem, GrammarAST } from 'langium';
910
import { createLangiumGrammarServices } from 'langium/grammar';
1011

12+
interface TestNode extends AstNode {
13+
readonly $type: 'MyType';
14+
readonly $container?: TestNode;
15+
str: string;
16+
strArr: string[]
17+
optStr?: string;
18+
optStrArr?: string[];
19+
bool: boolean;
20+
boolArr: boolean[];
21+
optBool?: boolean;
22+
optBoolArr?: boolean[];
23+
int: number;
24+
intArr: number[];
25+
optInt?: number;
26+
optIntArr?: number[];
27+
ref: Reference<TestNode>;
28+
refArr: Array<Reference<TestNode>>;
29+
optRef?: Reference<TestNode>;
30+
optRefArr?: Array<Reference<TestNode>>;
31+
ctn: TestNode;
32+
ctnArr: TestNode[];
33+
optCtn?: TestNode;
34+
optCtnArr?: PartialTestNode[];
35+
}
36+
37+
interface PartialTestNode extends AstNode {
38+
readonly $type: 'MyType';
39+
readonly $container?: PartialTestNode;
40+
str?: string;
41+
strArr: string[]
42+
optStr?: string;
43+
optStrArr?: string[];
44+
bool: boolean;
45+
boolArr: boolean[];
46+
optBool?: boolean;
47+
optBoolArr?: boolean[];
48+
int?: number;
49+
intArr: number[];
50+
optInt?: number;
51+
optIntArr?: number[];
52+
ref?: Reference<PartialTestNode>;
53+
refArr: Array<Reference<PartialTestNode>>;
54+
optRef?: Reference<PartialTestNode>;
55+
optRefArr?: Array<Reference<PartialTestNode>>;
56+
ctn?: PartialTestNode;
57+
ctnArr: PartialTestNode[];
58+
optCtn?: PartialTestNode;
59+
optCtnArr?: PartialTestNode[];
60+
}
61+
export const expectType = <Type>(_: Type): void => void 0;
62+
1163
describe('AST Utils', () => {
1264

1365
test('Streaming ast works with range', () => {
@@ -41,4 +93,9 @@ describe('AST Utils', () => {
4193
expect(names).toEqual(['OverlapBefore', 'Inside', 'OverlapAfter']);
4294
});
4395

96+
test('should transform DeepPartialAstNode<TestNode> to PartialTestNode', () => {
97+
type ResultType = AstUtils.DeepPartialAstNode<TestNode>;
98+
expectType<PartialTestNode>((null as unknown) as ResultType);
99+
expectType<ResultType>((null as unknown) as PartialTestNode);
100+
});
44101
});

0 commit comments

Comments
 (0)