Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate isType function for primitive datatype rules #1003

Merged
merged 7 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/domainmodel/src/language-server/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export function isAbstractElement(item: unknown): item is AbstractElement {

export type QualifiedName = string;

export function isQualifiedName(item: unknown): item is QualifiedName {
return typeof item === 'string';
}

export type Type = DataType | Entity;

export const Type = 'Type';
Expand Down
241 changes: 241 additions & 0 deletions packages/langium-cli/test/generator/ast-generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/******************************************************************************
* Copyright 2023 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, expect, test } from 'vitest';
import { createLangiumGrammarServices, EmptyFileSystem, expandToString, Grammar, normalizeEOL } from 'langium';
import { parseHelper } from 'langium/test';
import { LangiumConfig, RelativePath } from '../../src/package';
import { generateAst } from '../../src/generator/ast-generator';

const services = createLangiumGrammarServices(EmptyFileSystem);
const parse = parseHelper<Grammar>(services.grammar);

describe('Ast generator', () => {

testGeneratedAst('should generate checker functions for datatype rules comprised of a single string', `
grammar TestGrammar

A returns string:
'a';

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = 'a';

export function isA(item: unknown): item is A {
return item === 'a';
}
`);

testGeneratedAst('should generate checker functions for datatype rules comprised of a multiple strings', `
grammar TestGrammar

A returns string:
'a' | 'b' | 'c';

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = 'a' | 'b' | 'c';

export function isA(item: unknown): item is A {
return item === 'a' || item === 'b' || item === 'c';
}
`);

testGeneratedAst('should generate checker functions for datatype rules with subtypes', `
grammar TestGrammar

A returns string:
'a';

AB returns string:
A | 'b';

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = 'a';

export function isA(item: unknown): item is A {
return item === 'a';
}

export type AB = 'b' | A;

export function isAB(item: unknown): item is AB {
return isA(item) || item === 'b';
}
`);

testGeneratedAst('should generate checker functions for datatype rules referencing a terminal', `
grammar TestGrammar

A returns string:
ID;

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = string;

export function isA(item: unknown): item is A {
return (typeof item === 'string' && (/[_a-zA-Z][\\w_]*/.test(item)));
}
`);

testGeneratedAst('should generate checker functions for datatype rules referencing multiple terminals', `
grammar TestGrammar

A returns string:
ID | STRING;

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
terminal STRING: /"(\\\\.|[^"\\\\])*"|'(\\\\.|[^'\\\\])*'/;
`, expandToString`
export type A = string;

export function isA(item: unknown): item is A {
return (typeof item === 'string' && (/[_a-zA-Z][\\w_]*/.test(item) || /"(\\\\.|[^"\\\\])*"|'(\\\\.|[^'\\\\])*'/.test(item)));
}
`);

testGeneratedAst('should generate checker functions for datatype rules with nested union', `
grammar TestGrammar

A returns string:
'a';

B returns string:
'b';

C returns string:
'c';

ABC returns string:
A | ( B | C );

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = 'a';

export function isA(item: unknown): item is A {
return item === 'a';
}

export type ABC = (B | C) | A;

export function isABC(item: unknown): item is ABC {
return isA(item) || isB(item) || isC(item);
}

export type B = 'b';

export function isB(item: unknown): item is B {
return item === 'b';
}

export type C = 'c';

export function isC(item: unknown): item is C {
return item === 'c';
}
`);

testGeneratedAst('should generate checker functions for datatype rules with repeated terminals', `
grammar TestGrammar

A returns string:
ID ('.' ID)*;

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = string;

export function isA(item: unknown): item is A {
return typeof item === 'string';
}
`);

testGeneratedAst('should generate checker functions for datatype rules of type number', `
grammar TestGrammar

A returns number: '1';

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = number;

export function isA(item: unknown): item is A {
return typeof item === 'number';
}
`);

testGeneratedAst('should generate checker functions for datatype rules of type boolean', `
grammar TestGrammar

A returns boolean: 'on';

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = boolean;

export function isA(item: unknown): item is A {
return typeof item === 'boolean';
}
`);

testGeneratedAst('should generate checker functions for datatype rules of type bigint', `
grammar TestGrammar

A returns bigint: '1';

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = bigint;

export function isA(item: unknown): item is A {
return typeof item === 'bigint';
}
`);

testGeneratedAst('should generate checker functions for datatype rules of type Date', `
grammar TestGrammar

A returns Date: '2023-01-01';

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
`, expandToString`
export type A = Date;

export function isA(item: unknown): item is A {
return item instanceof Date;
}
`);
});

function testGeneratedAst(name: string, grammar: string, expected: string): void {
test(name, async () => {
const result = (await parse(grammar)).parseResult;
const config: LangiumConfig = {
[RelativePath]: './',
projectName: 'test',
languages: []
};
const expectedPart = normalizeEOL(expected).trim();
const typesFileContent = generateAst(services.grammar, [result.value], config);
const relevantPart = typesFileContent.substring(typesFileContent.indexOf('export'), typesFileContent.indexOf('export type testAstType')).trim();
expect(relevantPart).toEqual(expectedPart);
});
}
8 changes: 8 additions & 0 deletions packages/langium/src/grammar/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,16 @@ export function isCondition(item: unknown): item is Condition {

export type FeatureName = 'current' | 'entry' | 'extends' | 'false' | 'fragment' | 'grammar' | 'hidden' | 'import' | 'infer' | 'infers' | 'interface' | 'returns' | 'terminal' | 'true' | 'type' | 'with' | PrimitiveType | string;

export function isFeatureName(item: unknown): item is FeatureName {
return isPrimitiveType(item) || item === 'current' || item === 'entry' || item === 'extends' || item === 'false' || item === 'fragment' || item === 'grammar' || item === 'hidden' || item === 'import' || item === 'interface' || item === 'returns' || item === 'terminal' || item === 'true' || item === 'type' || item === 'infer' || item === 'infers' || item === 'with' || (typeof item === 'string' && (/\^?[_a-zA-Z][\w_]*/.test(item)));
}

export type PrimitiveType = 'Date' | 'bigint' | 'boolean' | 'number' | 'string';

export function isPrimitiveType(item: unknown): item is PrimitiveType {
return item === 'string' || item === 'number' || item === 'boolean' || item === 'Date' || item === 'bigint';
}

export type TypeDefinition = ArrayType | ReferenceType | SimpleType | UnionType;

export const TypeDefinition = 'TypeDefinition';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { isNamed } from '../../../references/name-provider';
import { MultiMap } from '../../../utils/collections';
import { ParserRule, isAlternatives, isKeyword, Action, isParserRule, isAction, AbstractElement, isGroup, isUnorderedGroup, isAssignment, isRuleCall, Assignment, isCrossReference, RuleCall, isTerminalRule } from '../../generated/ast';
import { getTypeNameWithoutError, isOptionalCardinality, getRuleType, isPrimitiveType } from '../../internal-grammar-util';
import { getTypeNameWithoutError, isOptionalCardinality, getRuleType, isPrimitiveType, terminalRegex } from '../../internal-grammar-util';
import { mergePropertyTypes, PlainAstTypes, PlainInterface, PlainProperty, PlainPropertyType, PlainUnion } from './plain-types';

interface TypePart {
Expand Down Expand Up @@ -231,7 +231,8 @@ export function collectInferredTypes(parserRules: ParserRule[], datatypeRules: P
declared: false,
type,
subTypes: new Set(),
superTypes: new Set()
superTypes: new Set(),
dataType: rule.dataType,
});
}
return astTypes;
Expand Down Expand Up @@ -280,7 +281,8 @@ function buildDataRuleType(element: AbstractElement, cancel: () => PlainProperty
if (ref) {
if (isTerminalRule(ref)) {
return {
primitive: ref.type?.name ?? 'string'
primitive: ref.type?.name ?? 'string',
regex: terminalRegex(ref)
};
} else {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface PlainUnion {
subTypes: Set<string>;
type: PlainPropertyType;
declared: boolean;
dataType?: string;
}

export function isPlainUnion(type: PlainType): type is PlainUnion {
Expand Down Expand Up @@ -88,6 +89,7 @@ export function isPlainValueType(propertyType: PlainPropertyType): propertyType

export interface PlainPrimitiveType {
primitive: string;
regex?: string;
}

export function isPlainPrimitiveType(propertyType: PlainPropertyType): propertyType is PlainPrimitiveType {
Expand All @@ -111,7 +113,8 @@ export function plainToTypes(plain: PlainAstTypes): AstTypes {
}
for (const unionValue of plain.unions) {
const type = new UnionType(unionValue.name, {
declared: unionValue.declared
declared: unionValue.declared,
dataType: unionValue.dataType
});
unionTypes.set(unionValue.name, type);
}
Expand Down Expand Up @@ -172,7 +175,8 @@ function plainToPropertyType(type: PlainPropertyType, union: UnionType | undefin
};
} else if (isPlainPrimitiveType(type)) {
return {
primitive: type.primitive
primitive: type.primitive,
regex: type.regex
};
} else if (isPlainValueType(type)) {
const value = interfaces.get(type.value) || unions.get(type.value);
Expand Down Expand Up @@ -231,4 +235,4 @@ export function flattenPlainType(type: PlainPropertyType): PlainPropertyType[] {
} else {
return [type];
}
}
}
Loading