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 2 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
109 changes: 109 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,109 @@
/******************************************************************************
* 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, 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);

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

test('should generate checker functions for datatype rules', async () => {
const result = (await parseHelper<Grammar>(services.grammar)(TEST_GRAMMAR)).parseResult;
const config: LangiumConfig = {
[RelativePath]: './',
projectName: 'test',
languages: []
};
const typesFileContent = generateAst(services.grammar, [result.value], config);
expect(typesFileContent).toEqual(EXPECTED_AST_FILE);
});
});

const TEST_GRAMMAR = `
grammar Arithmetics

A returns string:
'a';

AB returns string:
A | 'b';

ABC returns string:
AB | ID;

hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;

hidden terminal ML_COMMENT: /\\/\\*[\\s\\S]*?\\*\\//;
hidden terminal SL_COMMENT: /\\/\\/[^\\n\\r]*/;
`;

const EXPECTED_AST_FILE = normalizeEOL(`/******************************************************************************
msujew marked this conversation as resolved.
Show resolved Hide resolved
* This file was generated by langium-cli 1.1.0.
* DO NOT EDIT MANUALLY!
******************************************************************************/

/* eslint-disable */
import { AstNode, AbstractAstReflection, ReferenceInfo, TypeMetaData } from 'langium';

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

export type AB = 'b' | A;
export function isAB(item: string): item is AB {
return isA(item) || item === 'b';
}

export type ABC = AB | string;
export function isABC(item: string): item is ABC {
return isAB(item) || /[_a-zA-Z][\\w_]*/.test(item);
}

export type testAstType = {
}

export class testAstReflection extends AbstractAstReflection {

getAllTypes(): string[] {
return [];
}

protected override computeIsSubtype(subtype: string, supertype: string): boolean {
switch (subtype) {
default: {
return false;
}
}
}

getReferenceType(refInfo: ReferenceInfo): string {
const referenceId = \`\${refInfo.container.$type}:\${refInfo.property}\`;
switch (referenceId) {
default: {
throw new Error(\`\${referenceId} is not a valid reference id.\`);
}
}
}

getTypeMetaData(type: string): TypeMetaData {
switch (type) {
default: {
return {
name: type,
mandatory: []
};
}
}
}
}

export const reflection = new testAstReflection();
`);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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 { ParserRule, isAlternatives, isKeyword, Action, isParserRule, isAction, AbstractElement, isGroup, isUnorderedGroup, isAssignment, isRuleCall, Assignment, isCrossReference, RuleCall, isTerminalRule, isRegexToken } from '../../generated/ast';
import { getTypeNameWithoutError, isOptionalCardinality, getRuleType, isPrimitiveType } from '../../internal-grammar-util';
import { mergePropertyTypes, PlainAstTypes, PlainInterface, PlainProperty, PlainPropertyType, PlainUnion } from './plain-types';

Expand Down Expand Up @@ -279,8 +279,13 @@ function buildDataRuleType(element: AbstractElement, cancel: () => PlainProperty
const ref = element.rule?.ref;
if (ref) {
if (isTerminalRule(ref)) {
let regex;
if (isRegexToken(ref.definition)) {
regex = ref.definition.regex;
}
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
return {
primitive: ref.type?.name ?? 'string'
primitive: ref.type?.name ?? 'string',
regex: regex
};
} else {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function isPlainValueType(propertyType: PlainPropertyType): propertyType

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

export function isPlainPrimitiveType(propertyType: PlainPropertyType): propertyType is PlainPrimitiveType {
Expand Down Expand Up @@ -172,7 +173,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 +233,4 @@ export function flattenPlainType(type: PlainPropertyType): PlainPropertyType[] {
} else {
return [type];
}
}
}
120 changes: 120 additions & 0 deletions packages/langium/src/grammar/type-system/type-collector/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function isValueType(propertyType: PropertyType): propertyType is ValueTy

export interface PrimitiveType {
primitive: string
regex?: string
}

export function isPrimitiveType(propertyType: PropertyType): propertyType is PrimitiveType {
Expand Down Expand Up @@ -123,6 +124,17 @@ export class UnionType {
unionNode.append(NL);
pushReflectionInfo(unionNode, this.name);
}

if (isStringType(this.type)) {
pushDataTypeReflectionInfo(unionNode, this);
}

if (isPropertyUnion(this.type)) {
if (containsOnlyStringTypes(this)) {
pushDataTypeReflectionInfo(unionNode, this);
}
}
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved

return toString(unionNode);
}

Expand Down Expand Up @@ -376,6 +388,114 @@ function pushReflectionInfo(node: CompositeGeneratorNode, name: string) {
node.append('}', NL);
}

function pushDataTypeReflectionInfo(node: CompositeGeneratorNode, union: UnionType) {
const subTypes = Array.from(union.subTypes).map(e => e.name);
const strings = collectStringValuesFromDataType(union);
const regexes = collectRegexesFromDataType(union);
const returnString = createDataTypeCheckerFunctionReturnString(subTypes, strings, regexes);
node.append(`export function is${union.name}(item: string): item is ${union.name} {`, NL);
node.indent(body => {
body.append(returnString + ';', NL);
});
node.append('}', NL);
}

function createDataTypeCheckerFunctionReturnString(subTypes: string[], strings: string[], regexes: string[]): string {
msujew marked this conversation as resolved.
Show resolved Hide resolved
let returnString = 'return ';
if (subTypes.length > 0) {
for (let i = 0; i < subTypes.length; i++) {
returnString += `is${subTypes[i]}(item)`;
if (i < subTypes.length - 1) {
returnString += ' || ';
}
}
}
if (subTypes.length > 0 && strings.length > 0) {
returnString += ' || ';
}
if (strings.length > 0) {
for (let i = 0; i < strings.length; i++) {
returnString += `item === '${strings[i]}'`;
if (i < strings.length - 1) {
returnString += ' || ';
}
}
}
if ((subTypes.length > 0 || strings.length >0 ) && regexes.length > 0) {
returnString += ' || ';
}
if (regexes.length > 0) {
for (let i = 0; i < regexes.length; i++) {
returnString += `/${regexes[i]}/.test(item)`;
if (i < regexes.length - 1) {
returnString +=' || ';
}
}
}
return returnString;
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
}

function escapeReservedWords(name: string, reserved: Set<string>): string {
return reserved.has(name) ? `^${name}` : name;
}

function containsOnlyStringTypes(union: UnionType): boolean {
let result = true;
if(isPrimitiveType(union.type)) {
if (union.type.primitive === 'string' && union.type.regex) {
return true;
} else {
return false;
}
}
if (isStringType(union.type)) {
return true;
}
if (!isPropertyUnion(union.type)) {
return false;
} else {
for(const type of union.type.types) {
if (isValueType(type) && isUnionType(type.value)) {
result = containsOnlyStringTypes(type.value);
if (!result) break;
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
} else if (isStringType(type) ){
return true;
} else if (isPrimitiveType(type)) {
if (type.primitive === 'string' && type.regex) {
return true;
} else {
return false;
}
} else {
return false;
}
}
}
return result;
}

function collectStringValuesFromDataType(union: UnionType): string[] {
const values: string[] = [];
if (isStringType(union.type)) {
return [union.type.string];
}
if (isPropertyUnion(union.type)) {
const strings = union.type.types.filter(e => isStringType(e)).map(e => (e as StringType).string);
values.push(...strings);
}
return values;
}

function collectRegexesFromDataType(union: UnionType): string[] {
const regexes: string[] = [];
if (isPrimitiveType(union.type) && union.type.primitive === 'string' && union.type.regex) {
regexes.push(union.type.regex);
}
if (isPropertyUnion(union.type)) {
const regexesArray = union.type.types.filter(e => isPrimitiveType(e) && e.primitive === 'string' && e.regex);
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
if (regexesArray.length > 0) {
regexes.push(...regexesArray.map(e => (e as PrimitiveType).regex!));
}
}
return regexes;
}
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,13 @@ describe('Inferred types', () => {
export type Complex = string;
export type DateLike = Date;
export type MoreStrings = 'd' | 'e' | Strings;
export function isMoreStrings(item: string): item is MoreStrings {
return isStrings(item) || item === 'd' || item === 'e';
}
export type Strings = 'a' | 'b' | 'c';
export function isStrings(item: string): item is Strings {
return item === 'a' || item === 'b' || item === 'c';
}
`);
});

Expand Down Expand Up @@ -735,5 +741,5 @@ async function getTypes(grammar: string): Promise<AstTypes> {
async function expectTypes(grammar: string, types: string): Promise<void> {
const grammarTypes = await getTypes(grammar);
const allTypes = mergeTypesAndInterfaces(grammarTypes);
expect(allTypes.map(type => type.toAstTypesString(false)).join('').trim()).toBe(types);
expect(allTypes.map(e => e.toAstTypesString(false)).join('').trim()).toBe(types);
}