diff --git a/examples/arithmetics/src/language-server/generated/ast.ts b/examples/arithmetics/src/language-server/generated/ast.ts index 9ae60b4d4..19de1607c 100644 --- a/examples/arithmetics/src/language-server/generated/ast.ts +++ b/examples/arithmetics/src/language-server/generated/ast.ts @@ -229,7 +229,8 @@ export class ArithmeticsAstReflection extends langium.AbstractAstReflection { properties: { args: { name: Definition.args, - defaultValue: [] + defaultValue: [], + optional: true }, expr: { name: Definition.expr @@ -260,7 +261,8 @@ export class ArithmeticsAstReflection extends langium.AbstractAstReflection { properties: { args: { name: FunctionCall.args, - defaultValue: [] + defaultValue: [], + optional: true }, func: { name: FunctionCall.func, @@ -277,7 +279,8 @@ export class ArithmeticsAstReflection extends langium.AbstractAstReflection { }, statements: { name: Module.statements, - defaultValue: [] + defaultValue: [], + optional: true } }, superTypes: [] diff --git a/examples/domainmodel/src/language-server/generated/ast.ts b/examples/domainmodel/src/language-server/generated/ast.ts index b7624efbe..db4173bdc 100644 --- a/examples/domainmodel/src/language-server/generated/ast.ts +++ b/examples/domainmodel/src/language-server/generated/ast.ts @@ -170,7 +170,8 @@ export class DomainModelAstReflection extends langium.AbstractAstReflection { properties: { elements: { name: Domainmodel.elements, - defaultValue: [] + defaultValue: [], + optional: true } }, superTypes: [] @@ -180,14 +181,16 @@ export class DomainModelAstReflection extends langium.AbstractAstReflection { properties: { features: { name: Entity.features, - defaultValue: [] + defaultValue: [], + optional: true }, name: { name: Entity.name }, superType: { name: Entity.superType, - referenceType: Entity.$type + referenceType: Entity.$type, + optional: true } }, superTypes: [Type.$type] @@ -197,7 +200,8 @@ export class DomainModelAstReflection extends langium.AbstractAstReflection { properties: { many: { name: Feature.many, - defaultValue: false + defaultValue: false, + optional: true }, name: { name: Feature.name @@ -214,7 +218,8 @@ export class DomainModelAstReflection extends langium.AbstractAstReflection { properties: { elements: { name: PackageDeclaration.elements, - defaultValue: [] + defaultValue: [], + optional: true }, name: { name: PackageDeclaration.name diff --git a/examples/requirements/src/language-server/generated/ast.ts b/examples/requirements/src/language-server/generated/ast.ts index a1414dc10..a06d615b9 100644 --- a/examples/requirements/src/language-server/generated/ast.ts +++ b/examples/requirements/src/language-server/generated/ast.ts @@ -230,7 +230,8 @@ export class RequirementsAndTestsAstReflection extends langium.AbstractAstReflec environments: { name: Requirement.environments, defaultValue: [], - referenceType: Environment.$type + referenceType: Environment.$type, + optional: true }, name: { name: Requirement.name @@ -245,15 +246,18 @@ export class RequirementsAndTestsAstReflection extends langium.AbstractAstReflec name: RequirementModel.$type, properties: { contact: { - name: RequirementModel.contact + name: RequirementModel.contact, + optional: true }, environments: { name: RequirementModel.environments, - defaultValue: [] + defaultValue: [], + optional: true }, requirements: { name: RequirementModel.requirements, - defaultValue: [] + defaultValue: [], + optional: true } }, superTypes: [] @@ -264,7 +268,8 @@ export class RequirementsAndTestsAstReflection extends langium.AbstractAstReflec environments: { name: Test.environments, defaultValue: [], - referenceType: Environment.$type + referenceType: Environment.$type, + optional: true }, name: { name: Test.name @@ -275,7 +280,8 @@ export class RequirementsAndTestsAstReflection extends langium.AbstractAstReflec referenceType: Requirement.$type }, testFile: { - name: Test.testFile + name: Test.testFile, + optional: true } }, superTypes: [] @@ -284,11 +290,13 @@ export class RequirementsAndTestsAstReflection extends langium.AbstractAstReflec name: TestModel.$type, properties: { contact: { - name: TestModel.contact + name: TestModel.contact, + optional: true }, tests: { name: TestModel.tests, - defaultValue: [] + defaultValue: [], + optional: true } }, superTypes: [] diff --git a/examples/statemachine/src/language-server/generated/ast.ts b/examples/statemachine/src/language-server/generated/ast.ts index 3f2b02e64..7b94cb33e 100644 --- a/examples/statemachine/src/language-server/generated/ast.ts +++ b/examples/statemachine/src/language-server/generated/ast.ts @@ -162,14 +162,16 @@ export class StatemachineAstReflection extends langium.AbstractAstReflection { actions: { name: State.actions, defaultValue: [], - referenceType: Command.$type + referenceType: Command.$type, + optional: true }, name: { name: State.name }, transitions: { name: State.transitions, - defaultValue: [] + defaultValue: [], + optional: true } }, superTypes: [] @@ -179,11 +181,13 @@ export class StatemachineAstReflection extends langium.AbstractAstReflection { properties: { commands: { name: Statemachine.commands, - defaultValue: [] + defaultValue: [], + optional: true }, events: { name: Statemachine.events, - defaultValue: [] + defaultValue: [], + optional: true }, init: { name: Statemachine.init, @@ -194,7 +198,8 @@ export class StatemachineAstReflection extends langium.AbstractAstReflection { }, states: { name: Statemachine.states, - defaultValue: [] + defaultValue: [], + optional: true } }, superTypes: [] diff --git a/packages/langium-cli/src/generator/ast-generator.ts b/packages/langium-cli/src/generator/ast-generator.ts index 9902ebce5..87dd73c87 100644 --- a/packages/langium-cli/src/generator/ast-generator.ts +++ b/packages/langium-cli/src/generator/ast-generator.ts @@ -180,6 +180,9 @@ function buildPropertyMetaData(props: Property[], ownerTypeName: string): Genera if (refType) { attributes.push(`referenceType: ${refType}.$type`); } + if (property.optional) { + attributes.push('optional: true'); + } return expandToNode` ${property.name}: { diff --git a/packages/langium/src/grammar/ast-reflection-interpreter.ts b/packages/langium/src/grammar/ast-reflection-interpreter.ts index 6e11875d0..3ccf22fad 100644 --- a/packages/langium/src/grammar/ast-reflection-interpreter.ts +++ b/packages/langium/src/grammar/ast-reflection-interpreter.ts @@ -145,7 +145,8 @@ function buildPropertyMetaData(props: Property[]): { [name: string]: PropertyMet for (const property of all) { properties[property.name] = { name: property.name, - defaultValue: property.defaultValue + defaultValue: property.defaultValue, + ...(property.optional ? { optional: true } : {}) }; } return properties; diff --git a/packages/langium/src/languages/generated/ast.ts b/packages/langium/src/languages/generated/ast.ts index 7493b8d5b..c7f4a2ede 100644 --- a/packages/langium/src/languages/generated/ast.ts +++ b/packages/langium/src/languages/generated/ast.ts @@ -994,7 +994,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: AbstractElement.$type, properties: { cardinality: { - name: AbstractElement.cardinality + name: AbstractElement.cardinality, + optional: true } }, superTypes: [] @@ -1021,20 +1022,25 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: Action.$type, properties: { cardinality: { - name: Action.cardinality + name: Action.cardinality, + optional: true }, feature: { - name: Action.feature + name: Action.feature, + optional: true }, inferredType: { - name: Action.inferredType + name: Action.inferredType, + optional: true }, operator: { - name: Action.operator + name: Action.operator, + optional: true }, type: { name: Action.type, - referenceType: AbstractType.$type + referenceType: AbstractType.$type, + optional: true } }, superTypes: [AbstractElement.$type] @@ -1043,7 +1049,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: Alternatives.$type, properties: { cardinality: { - name: Alternatives.cardinality + name: Alternatives.cardinality, + optional: true }, elements: { name: Alternatives.elements, @@ -1075,7 +1082,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: Assignment.$type, properties: { cardinality: { - name: Assignment.cardinality + name: Assignment.cardinality, + optional: true }, feature: { name: Assignment.feature @@ -1084,7 +1092,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: Assignment.operator }, predicate: { - name: Assignment.predicate + name: Assignment.predicate, + optional: true }, terminal: { name: Assignment.terminal @@ -1106,20 +1115,24 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: CharacterRange.$type, properties: { cardinality: { - name: CharacterRange.cardinality + name: CharacterRange.cardinality, + optional: true }, left: { name: CharacterRange.left }, lookahead: { - name: CharacterRange.lookahead + name: CharacterRange.lookahead, + optional: true }, parenthesized: { name: CharacterRange.parenthesized, - defaultValue: false + defaultValue: false, + optional: true }, right: { - name: CharacterRange.right + name: CharacterRange.right, + optional: true } }, superTypes: [TerminalElement.$type] @@ -1146,7 +1159,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: CrossReference.$type, properties: { cardinality: { - name: CrossReference.cardinality + name: CrossReference.cardinality, + optional: true }, deprecatedSyntax: { name: CrossReference.deprecatedSyntax, @@ -1157,7 +1171,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { defaultValue: false }, terminal: { - name: CrossReference.terminal + name: CrossReference.terminal, + optional: true }, type: { name: CrossReference.type, @@ -1182,7 +1197,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: EndOfFile.$type, properties: { cardinality: { - name: EndOfFile.cardinality + name: EndOfFile.cardinality, + optional: true } }, superTypes: [AbstractElement.$type] @@ -1203,7 +1219,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { defaultValue: false }, name: { - name: Grammar.name + name: Grammar.name, + optional: true }, rules: { name: Grammar.rules, @@ -1229,17 +1246,20 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: Group.$type, properties: { cardinality: { - name: Group.cardinality + name: Group.cardinality, + optional: true }, elements: { name: Group.elements, defaultValue: [] }, guardCondition: { - name: Group.guardCondition + name: Group.guardCondition, + optional: true }, predicate: { - name: Group.predicate + name: Group.predicate, + optional: true } }, superTypes: [AbstractElement.$type] @@ -1260,10 +1280,12 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: InfixRule.call }, dataType: { - name: InfixRule.dataType + name: InfixRule.dataType, + optional: true }, inferredType: { - name: InfixRule.inferredType + name: InfixRule.inferredType, + optional: true }, name: { name: InfixRule.name @@ -1277,7 +1299,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { }, returnType: { name: InfixRule.returnType, - referenceType: AbstractType.$type + referenceType: AbstractType.$type, + optional: true } }, superTypes: [AbstractParserRule.$type] @@ -1286,7 +1309,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: InfixRuleOperatorList.$type, properties: { associativity: { - name: InfixRuleOperatorList.associativity + name: InfixRuleOperatorList.associativity, + optional: true }, operators: { name: InfixRuleOperatorList.operators, @@ -1327,10 +1351,12 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: Keyword.$type, properties: { cardinality: { - name: Keyword.cardinality + name: Keyword.cardinality, + optional: true }, predicate: { - name: Keyword.predicate + name: Keyword.predicate, + optional: true }, value: { name: Keyword.value @@ -1347,7 +1373,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { }, parameter: { name: NamedArgument.parameter, - referenceType: Parameter.$type + referenceType: Parameter.$type, + optional: true }, value: { name: NamedArgument.value @@ -1359,14 +1386,17 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: NegatedToken.$type, properties: { cardinality: { - name: NegatedToken.cardinality + name: NegatedToken.cardinality, + optional: true }, lookahead: { - name: NegatedToken.lookahead + name: NegatedToken.lookahead, + optional: true }, parenthesized: { name: NegatedToken.parenthesized, - defaultValue: false + defaultValue: false, + optional: true }, terminal: { name: NegatedToken.terminal @@ -1415,7 +1445,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: ParserRule.$type, properties: { dataType: { - name: ParserRule.dataType + name: ParserRule.dataType, + optional: true }, definition: { name: ParserRule.definition @@ -1429,7 +1460,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { defaultValue: false }, inferredType: { - name: ParserRule.inferredType + name: ParserRule.inferredType, + optional: true }, name: { name: ParserRule.name @@ -1440,7 +1472,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { }, returnType: { name: ParserRule.returnType, - referenceType: AbstractType.$type + referenceType: AbstractType.$type, + optional: true } }, superTypes: [AbstractParserRule.$type] @@ -1462,14 +1495,17 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: RegexToken.$type, properties: { cardinality: { - name: RegexToken.cardinality + name: RegexToken.cardinality, + optional: true }, lookahead: { - name: RegexToken.lookahead + name: RegexToken.lookahead, + optional: true }, parenthesized: { name: RegexToken.parenthesized, - defaultValue: false + defaultValue: false, + optional: true }, regex: { name: RegexToken.regex @@ -1494,10 +1530,12 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { defaultValue: [] }, cardinality: { - name: RuleCall.cardinality + name: RuleCall.cardinality, + optional: true }, predicate: { - name: RuleCall.predicate + name: RuleCall.predicate, + optional: true }, rule: { name: RuleCall.rule, @@ -1510,14 +1548,17 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: SimpleType.$type, properties: { primitiveType: { - name: SimpleType.primitiveType + name: SimpleType.primitiveType, + optional: true }, stringType: { - name: SimpleType.stringType + name: SimpleType.stringType, + optional: true }, typeRef: { name: SimpleType.typeRef, - referenceType: AbstractType.$type + referenceType: AbstractType.$type, + optional: true } }, superTypes: [TypeDefinition.$type] @@ -1535,18 +1576,21 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: TerminalAlternatives.$type, properties: { cardinality: { - name: TerminalAlternatives.cardinality + name: TerminalAlternatives.cardinality, + optional: true }, elements: { name: TerminalAlternatives.elements, defaultValue: [] }, lookahead: { - name: TerminalAlternatives.lookahead + name: TerminalAlternatives.lookahead, + optional: true }, parenthesized: { name: TerminalAlternatives.parenthesized, - defaultValue: false + defaultValue: false, + optional: true } }, superTypes: [TerminalElement.$type] @@ -1555,14 +1599,17 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: TerminalElement.$type, properties: { cardinality: { - name: TerminalElement.cardinality + name: TerminalElement.cardinality, + optional: true }, lookahead: { - name: TerminalElement.lookahead + name: TerminalElement.lookahead, + optional: true }, parenthesized: { name: TerminalElement.parenthesized, - defaultValue: false + defaultValue: false, + optional: true } }, superTypes: [AbstractElement.$type] @@ -1571,18 +1618,21 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: TerminalGroup.$type, properties: { cardinality: { - name: TerminalGroup.cardinality + name: TerminalGroup.cardinality, + optional: true }, elements: { name: TerminalGroup.elements, defaultValue: [] }, lookahead: { - name: TerminalGroup.lookahead + name: TerminalGroup.lookahead, + optional: true }, parenthesized: { name: TerminalGroup.parenthesized, - defaultValue: false + defaultValue: false, + optional: true } }, superTypes: [TerminalElement.$type] @@ -1605,7 +1655,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: TerminalRule.name }, type: { - name: TerminalRule.type + name: TerminalRule.type, + optional: true } }, superTypes: [AbstractRule.$type] @@ -1614,14 +1665,17 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: TerminalRuleCall.$type, properties: { cardinality: { - name: TerminalRuleCall.cardinality + name: TerminalRuleCall.cardinality, + optional: true }, lookahead: { - name: TerminalRuleCall.lookahead + name: TerminalRuleCall.lookahead, + optional: true }, parenthesized: { name: TerminalRuleCall.parenthesized, - defaultValue: false + defaultValue: false, + optional: true }, rule: { name: TerminalRuleCall.rule, @@ -1646,7 +1700,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: TypeAttribute.$type, properties: { defaultValue: { - name: TypeAttribute.defaultValue + name: TypeAttribute.defaultValue, + optional: true }, isOptional: { name: TypeAttribute.isOptional, @@ -1681,7 +1736,8 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: UnorderedGroup.$type, properties: { cardinality: { - name: UnorderedGroup.cardinality + name: UnorderedGroup.cardinality, + optional: true }, elements: { name: UnorderedGroup.elements, @@ -1694,14 +1750,17 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: UntilToken.$type, properties: { cardinality: { - name: UntilToken.cardinality + name: UntilToken.cardinality, + optional: true }, lookahead: { - name: UntilToken.lookahead + name: UntilToken.lookahead, + optional: true }, parenthesized: { name: UntilToken.parenthesized, - defaultValue: false + defaultValue: false, + optional: true }, terminal: { name: UntilToken.terminal @@ -1719,14 +1778,17 @@ export class LangiumGrammarAstReflection extends langium.AbstractAstReflection { name: Wildcard.$type, properties: { cardinality: { - name: Wildcard.cardinality + name: Wildcard.cardinality, + optional: true }, lookahead: { - name: Wildcard.lookahead + name: Wildcard.lookahead, + optional: true }, parenthesized: { name: Wildcard.parenthesized, - defaultValue: false + defaultValue: false, + optional: true } }, superTypes: [TerminalElement.$type] diff --git a/packages/langium/src/syntax-tree.ts b/packages/langium/src/syntax-tree.ts index 3c19e098d..c5cb237e9 100644 --- a/packages/langium/src/syntax-tree.ts +++ b/packages/langium/src/syntax-tree.ts @@ -172,6 +172,7 @@ export interface AstReflection { isInstance(node: unknown, type: string): boolean isSubtype(subtype: string, supertype: string): boolean getAllSubTypes(type: string): string[] + isComplete(node: AstNode): boolean } /** @@ -251,6 +252,27 @@ export abstract class AbstractAstReflection implements AstReflection { return types; } } + + /** + * Check whether a given AST node is missing any non-optional properties. + * Used to guard against incomplete nodes that deviate from their declared type, + * particularly when error recovery is in effect. + */ + isComplete(node: AstNode): boolean { + const meta = this.getTypeMetaData(node.$type); + for (const key of Object.keys(meta.properties)) { + const prop = meta.properties[key]; + if (prop.optional) { + continue; + } + + const value = (node as GenericAstNode)[prop.name]; + if (value === undefined || value === null) { + return false; + } + } + return true; + } } /** @@ -281,11 +303,19 @@ export interface PropertyMetaData { /** The name of this property. */ name: string /** - * Indicates that the property is mandatory in the AST node. + * Indicates that the property's value is mandatory in the AST node when present. * For example, if an AST node contains an array, but no elements of this array have been parsed, * we still expect an empty array instead of `undefined`. + * A property with a `defaultValue` may still be `optional`, in which case this indicates + * the property must have a value when it is present. */ defaultValue?: PropertyType + /** + * Indicates whether this property is optional in the AST node. + * Note that a node with a `defaultValue` may also be optional. + * `isComplete` relies on this info for runtime node completeness checks + */ + optional?: boolean /** * If the property is a reference, this is the type of the reference target. */ diff --git a/packages/langium/test/grammar/ast-reflection-interpreter.test.ts b/packages/langium/test/grammar/ast-reflection-interpreter.test.ts index ac6b87b35..5bc93c44a 100644 --- a/packages/langium/test/grammar/ast-reflection-interpreter.test.ts +++ b/packages/langium/test/grammar/ast-reflection-interpreter.test.ts @@ -5,7 +5,9 @@ ******************************************************************************/ import { describe, expect, test } from 'vitest'; +// import type { AstNode } from 'langium'; import { InterfaceType, interpretAstReflection } from 'langium/grammar'; +import type { GenericAstNode } from '../../lib/syntax-tree.js'; describe('AST reflection interpreter', () => { @@ -103,4 +105,105 @@ describe('AST reflection interpreter', () => { }); + describe('isComplete', () => { + + // example of a rule like `Var: 'var' name=ID '=' value=ID;` + // where name and value are required strings, + // and items is an array w/ a default + const varType = new InterfaceType('Var', false, false); + varType.properties.push({ + name: 'name', + astNodes: new Set(), + optional: false, + type: { + elementType: { primitive: 'string' } + } + }, { + name: 'value', + astNodes: new Set(), + optional: true, + type: { + elementType: { primitive: 'string' } + } + }, { + name: 'items', + astNodes: new Set(), + optional: false, + type: { + elementType: { primitive: 'string' } + }, + defaultValue: [] + }); + + // SubVar to extend Var w/ an 'extra' property + const subVarType = new InterfaceType('SubVar', false, false); + subVarType.properties.push({ + name: 'extra', + astNodes: new Set(), + optional: false, + type: { + elementType: { primitive: 'string' } + } + }); + subVarType.superTypes.add(varType); + + const reflection = interpretAstReflection({ + interfaces: [varType, subVarType], + unions: [] + }); + + test('complete node', () => { + expect(reflection.isComplete({ + $type: 'Var', + name: 'x', + value: 'hello', + items: [] + } as GenericAstNode)).toBe(true); + }); + + test('missing required name is incomplete', () => { + expect(reflection.isComplete({ + $type: 'Var', + value: 'hello', + items: [] + } as GenericAstNode)).toBe(false); + }); + + test('missing multiple required properties is incomplete', () => { + expect(reflection.isComplete({ + $type: 'Var', + items: [] + } as GenericAstNode)).toBe(false); + }); + + test('empty array is complete', () => { + expect(reflection.isComplete({ + $type: 'Var', + name: 'x', + value: 'y', + items: [] + } as GenericAstNode)).toBe(true); + }); + + test('missing inherited property is incomplete', () => { + expect(reflection.isComplete({ + $type: 'SubVar', + value: 'hello', + items: [], + extra: 'e' + } as GenericAstNode)).toBe(false); + }); + + test('complete subtype with all props is complete', () => { + expect(reflection.isComplete({ + $type: 'SubVar', + name: 'x', + value: 'hello', + items: [], + extra: 'e' + } as GenericAstNode)).toBe(true); + }); + + }); + });