Skip to content

Commit

Permalink
Improve validation check type safety (#738)
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew authored Nov 1, 2022
1 parent 9f58781 commit 4fd8469
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 19 deletions.
13 changes: 12 additions & 1 deletion examples/arithmetics/src/language-server/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,18 @@ export function isNumberLiteral(item: unknown): item is NumberLiteral {
return reflection.isInstance(item, NumberLiteral);
}

export type ArithmeticsAstType = 'AbstractDefinition' | 'BinaryExpression' | 'DeclaredParameter' | 'Definition' | 'Evaluation' | 'Expression' | 'FunctionCall' | 'Module' | 'NumberLiteral' | 'Statement';
export interface ArithmeticsAstType {
AbstractDefinition: AbstractDefinition
BinaryExpression: BinaryExpression
DeclaredParameter: DeclaredParameter
Definition: Definition
Evaluation: Evaluation
Expression: Expression
FunctionCall: FunctionCall
Module: Module
NumberLiteral: NumberLiteral
Statement: Statement
}

export class ArithmeticsAstReflection implements AstReflection {

Expand Down
10 changes: 9 additions & 1 deletion examples/domainmodel/src/language-server/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,15 @@ export function isPackageDeclaration(item: unknown): item is PackageDeclaration
return reflection.isInstance(item, PackageDeclaration);
}

export type DomainModelAstType = 'AbstractElement' | 'DataType' | 'Domainmodel' | 'Entity' | 'Feature' | 'PackageDeclaration' | 'Type';
export interface DomainModelAstType {
AbstractElement: AbstractElement
DataType: DataType
Domainmodel: Domainmodel
Entity: Entity
Feature: Feature
PackageDeclaration: PackageDeclaration
Type: Type
}

export class DomainModelAstReflection implements AstReflection {

Expand Down
9 changes: 8 additions & 1 deletion examples/requirements/src/language-server/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,14 @@ export function isTestModel(item: unknown): item is TestModel {
return reflection.isInstance(item, TestModel);
}

export type RequirementsAndTestsAstType = 'Contact' | 'Environment' | 'Requirement' | 'RequirementModel' | 'Test' | 'TestModel';
export interface RequirementsAndTestsAstType {
Contact: Contact
Environment: Environment
Requirement: Requirement
RequirementModel: RequirementModel
Test: Test
TestModel: TestModel
}

export class RequirementsAndTestsAstReflection implements AstReflection {

Expand Down
8 changes: 7 additions & 1 deletion examples/statemachine/src/language-server/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ export function isTransition(item: unknown): item is Transition {
return reflection.isInstance(item, Transition);
}

export type StatemachineAstType = 'Command' | 'Event' | 'State' | 'Statemachine' | 'Transition';
export interface StatemachineAstType {
Command: Command
Event: Event
State: State
Statemachine: Statemachine
Transition: Transition
}

export class StatemachineAstReflection implements AstReflection {

Expand Down
15 changes: 11 additions & 4 deletions packages/langium-cli/src/generator/ast-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,28 @@ function hasCrossReferences(grammar: Grammar): boolean {
}

function generateAstReflection(config: LangiumConfig, astTypes: AstTypes): GeneratorNode {
const typeNames: string[] = astTypes.interfaces.map(t => `'${t.name}'`)
.concat(astTypes.unions.map(t => `'${t.name}'`))
const typeNames: string[] = astTypes.interfaces.map(t => t.name)
.concat(astTypes.unions.map(t => t.name))
.sort();
const crossReferenceTypes = buildCrossReferenceTypes(astTypes);
const reflectionNode = new CompositeGeneratorNode();

reflectionNode.append(`export interface ${config.projectName}AstType {`, NL);
reflectionNode.indent(astTypeBody => {
for (const type of typeNames) {
astTypeBody.append(type, ': ', type, NL);
}
});
reflectionNode.append('}', NL, NL);

reflectionNode.append(
`export type ${config.projectName}AstType = ${typeNames.join(' | ')};`, NL, NL,
`export class ${config.projectName}AstReflection implements AstReflection {`, NL, NL
);

reflectionNode.indent(classBody => {
classBody.append('getAllTypes(): string[] {', NL);
classBody.indent(allTypes => {
allTypes.append(`return [${typeNames.join(', ')}];`, NL);
allTypes.append(`return [${typeNames.map(e => `'${e}'`).join(', ')}];`, NL);
});
classBody.append('}', NL, NL, 'isInstance(node: unknown, type: string): boolean {', NL);
classBody.indent(isInstance => {
Expand Down
40 changes: 39 additions & 1 deletion packages/langium/src/grammar/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,45 @@ export function isWildcard(item: unknown): item is Wildcard {
return reflection.isInstance(item, Wildcard);
}

export type LangiumGrammarAstType = 'AbstractElement' | 'AbstractRule' | 'AbstractType' | 'Action' | 'Alternatives' | 'Assignment' | 'AtomType' | 'CharacterRange' | 'Condition' | 'Conjunction' | 'CrossReference' | 'Disjunction' | 'Grammar' | 'GrammarImport' | 'Group' | 'InferredType' | 'Interface' | 'Keyword' | 'LiteralCondition' | 'NamedArgument' | 'NegatedToken' | 'Negation' | 'Parameter' | 'ParameterReference' | 'ParserRule' | 'RegexToken' | 'ReturnType' | 'RuleCall' | 'TerminalAlternatives' | 'TerminalGroup' | 'TerminalRule' | 'TerminalRuleCall' | 'Type' | 'TypeAttribute' | 'UnorderedGroup' | 'UntilToken' | 'Wildcard';
export interface LangiumGrammarAstType {
AbstractElement: AbstractElement
AbstractRule: AbstractRule
AbstractType: AbstractType
Action: Action
Alternatives: Alternatives
Assignment: Assignment
AtomType: AtomType
CharacterRange: CharacterRange
Condition: Condition
Conjunction: Conjunction
CrossReference: CrossReference
Disjunction: Disjunction
Grammar: Grammar
GrammarImport: GrammarImport
Group: Group
InferredType: InferredType
Interface: Interface
Keyword: Keyword
LiteralCondition: LiteralCondition
NamedArgument: NamedArgument
NegatedToken: NegatedToken
Negation: Negation
Parameter: Parameter
ParameterReference: ParameterReference
ParserRule: ParserRule
RegexToken: RegexToken
ReturnType: ReturnType
RuleCall: RuleCall
TerminalAlternatives: TerminalAlternatives
TerminalGroup: TerminalGroup
TerminalRule: TerminalRule
TerminalRuleCall: TerminalRuleCall
Type: Type
TypeAttribute: TypeAttribute
UnorderedGroup: UnorderedGroup
UntilToken: UntilToken
Wildcard: Wildcard
}

export class LangiumGrammarAstReflection implements AstReflection {

Expand Down
2 changes: 2 additions & 0 deletions packages/langium/src/syntax-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface GenericAstNode extends AstNode {
[key: string]: unknown
}

export type AstTypeList<T> = Record<keyof T, AstNode>;

type SpecificNodeProperties<N extends AstNode> = keyof Omit<N, keyof AstNode | number | symbol>;

/**
Expand Down
22 changes: 12 additions & 10 deletions packages/langium/src/validation/validation-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { CancellationToken, CodeDescription, DiagnosticRelatedInformation, DiagnosticTag, integer, Range } from 'vscode-languageserver';
import { LangiumServices } from '../services';
import { AstNode, AstReflection, Properties } from '../syntax-tree';
import { AstNode, AstReflection, AstTypeList, Properties } from '../syntax-tree';
import { MultiMap } from '../utils/collections';
import { isOperationCancelled, MaybePromise } from '../utils/promise-util';

Expand Down Expand Up @@ -35,10 +35,11 @@ export type DiagnosticInfo<N extends AstNode, P = Properties<N>> = {

export type ValidationAcceptor = <N extends AstNode>(severity: 'error' | 'warning' | 'info' | 'hint', message: string, info: DiagnosticInfo<N>) => void

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ValidationCheck = (node: any, accept: ValidationAcceptor, cancelToken: CancellationToken) => MaybePromise<void>;
export type ValidationCheck<T extends AstNode = AstNode> = (node: T, accept: ValidationAcceptor, cancelToken: CancellationToken) => MaybePromise<void>;

export type ValidationChecks<T extends string> = { [type in T]?: ValidationCheck | ValidationCheck[] }
export type ValidationChecks<T extends AstTypeList<T>> = {
[K in keyof T]?: ValidationCheck<T[K]> | Array<ValidationCheck<T[K]>>
}

/**
* Manages a set of `ValidationCheck`s to be applied when documents are validated.
Expand All @@ -51,14 +52,15 @@ export class ValidationRegistry {
this.reflection = services.shared.AstReflection;
}

register(checksRecord: { [type: string]: ValidationCheck | ValidationCheck[] | undefined }, thisObj: ThisParameterType<unknown> = this): void {
register<T extends AstTypeList<T>>(checksRecord: ValidationChecks<T>, thisObj: ThisParameterType<unknown> = this): void {
for (const [type, ch] of Object.entries(checksRecord)) {
if (Array.isArray(ch)) {
for (const check of ch) {
const callbacks = ch as ValidationCheck | ValidationCheck[];
if (Array.isArray(callbacks)) {
for (const check of callbacks) {
this.doRegister(type, this.wrapValidationException(check, thisObj));
}
} else if (ch) {
this.doRegister(type, this.wrapValidationException(ch, thisObj));
} else if (typeof callbacks === 'function') {
this.doRegister(type, this.wrapValidationException(callbacks, thisObj));
}
}
}
Expand All @@ -76,7 +78,7 @@ export class ValidationRegistry {
if(err instanceof Error && err.stack) {
console.error(err.stack);
}
accept('error', 'An error occurred during validation: ' + message, {node});
accept('error', 'An error occurred during validation: ' + message, { node });
}
};
}
Expand Down

0 comments on commit 4fd8469

Please sign in to comment.