Skip to content

Commit 65dfd32

Browse files
fixed bugs: circular calls of fragments, validate rewrite actions as starting points, more test cases
1 parent 7d11c3e commit 65dfd32

File tree

2 files changed

+230
-26
lines changed

2 files changed

+230
-26
lines changed

packages/langium/src/grammar/validation/validator.ts

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { AstNode, Properties, Reference } from '../../syntax-tree.js';
1313
import { getContainerOfType, getDocument, streamAllContents } from '../../utils/ast-utils.js';
1414
import { MultiMap } from '../../utils/collections.js';
1515
import { toDocumentSegment } from '../../utils/cst-utils.js';
16-
import { assertUnreachable } from '../../utils/errors.js';
16+
import { assertCondition, assertUnreachable } from '../../utils/errors.js';
1717
import { findNameAssignment, findNodeForKeyword, findNodeForProperty, getAllReachableRules, getAllRulesUsedForCrossReferences, isArrayCardinality, isDataTypeRule, isOptionalCardinality, terminalRegex } from '../../utils/grammar-utils.js';
1818
import type { Stream } from '../../utils/stream.js';
1919
import { stream } from '../../utils/stream.js';
@@ -44,6 +44,7 @@ export function registerValidationChecks(services: LangiumGrammarServices): void
4444
const checks: ValidationChecks<ast.LangiumGrammarAstType> = {
4545
Action: [
4646
validator.checkAssignmentReservedName,
47+
validator.checkActionAssignmentsToTheSameFeature,
4748
],
4849
AbstractRule: validator.checkRuleName,
4950
Assignment: [
@@ -58,7 +59,7 @@ export function registerValidationChecks(services: LangiumGrammarServices): void
5859
validator.checkRuleParameters,
5960
validator.checkEmptyParserRule,
6061
validator.checkParserRuleReservedName,
61-
validator.checkAssignmentsToTheSameFeature,
62+
validator.checkRuleAssignmentsToTheSameFeature,
6263
],
6364
InfixRule: [
6465
validator.checkInfixRuleDataType,
@@ -1020,20 +1021,47 @@ export class LangiumGrammarValidator {
10201021
}
10211022

10221023
/**
1023-
* This validation recursively collects all assignments (and rewriting actions) to the same feature and validates them:
1024+
* This validation recursively collects all assignments (and rewriting actions: see the check/entry point in the next function) to the same feature and validates them:
10241025
* Assignment operators '?=', '=' and '+=' should not be mixed.
10251026
* Assignments with '=' as assignment operator should be replaced be '+=', if assignments to the feature might occure more than once.
10261027
*/
1027-
checkAssignmentsToTheSameFeature(rule: ast.ParserRule, accept: ValidationAcceptor): void {
1028-
// for usual parser rules AND for fragments, but not for data type rules!
1029-
if (!rule.dataType) {
1030-
this.checkAssignmentsToTheSameFeatureIndependent(rule, [rule.definition], accept);
1028+
checkRuleAssignmentsToTheSameFeature(rule: ast.ParserRule, accept: ValidationAcceptor): void {
1029+
// validate only usual parser rules, but no fragments (they are validated when validating their using parser rules) and no data type rules!
1030+
if (!rule.dataType && !rule.fragment) {
1031+
this.checkAssignmentsToTheSameFeature(rule, [rule.definition], new Map(), accept);
10311032
}
10321033
}
10331034

1034-
private checkAssignmentsToTheSameFeatureIndependent(startRule: ast.ParserRule|ast.Action, startNodes: AstNode[], accept: ValidationAcceptor, map: Map<string, AssignmentUse> = new Map()): void {
1035+
checkActionAssignmentsToTheSameFeature(action: ast.Action, accept: ValidationAcceptor): void {
1036+
if (action.feature) {
1037+
// tree rewriting action
1038+
const mapForNewObject = new Map();
1039+
storeAssignmentUse(mapForNewObject, action.feature, 1, action); // remember the special rewriting feature
1040+
// all following nodes are put into the new object => check their assignments independently
1041+
const sibblings = ast.isGroup(action.$container) || ast.isUnorderedGroup(action.$container) ? action.$container.elements : [action];
1042+
this.checkAssignmentsToTheSameFeature(action, sibblings.slice(sibblings.indexOf(action) + 1), mapForNewObject, accept);
1043+
} else {
1044+
// this action does not create a new AstNode => no assignments to check
1045+
}
1046+
}
1047+
1048+
private checkAssignmentsToTheSameFeature(
1049+
startRule: ast.ParserRule|ast.Action, startNodes: AstNode[], map: Map<string, AssignmentUse>, accept: ValidationAcceptor
1050+
): void {
10351051
// check all starting nodes
1036-
this.checkAssignmentsToTheSameFeatureNested(startRule, startNodes, 1, map, accept);
1052+
const circularFragments = new Set<ast.ParserRule>();
1053+
this.checkAssignmentsToTheSameFeatureNested(startRule, startNodes, 1, map, [], circularFragments, accept);
1054+
1055+
// handle properties of fragments which are called in circular way => count its properties multiple times
1056+
for (const values of map.values()) {
1057+
for (const assignment of values.assignments) {
1058+
// for each found assignment, check whether it is defined inside a fragment which is called in circular way
1059+
const fragmentContainer = getContainerOfType(assignment, ast.isParserRule);
1060+
if (fragmentContainer && circularFragments.has(fragmentContainer)) {
1061+
values.counter += 1; // This calculation is not accurate, it is only important to know, that this assignment is called multiple times (>= 2).
1062+
}
1063+
}
1064+
}
10371065

10381066
// create the warnings
10391067
for (const entry of map.values()) {
@@ -1071,11 +1099,10 @@ export class LangiumGrammarValidator {
10711099
}
10721100
}
10731101

1074-
const startDocument = getDocument(startRule);
10751102
// Utility to handle the case, that the critical assignment is located in another document (for which validation markers cannot be reported now).
10761103
function createInfo(assignment: ast.Assignment | ast.Action, code: string): [DiagnosticInfo<ast.Assignment | ast.Action | ast.ParserRule>, string] {
10771104
const assignmentDocument = getDocument(assignment);
1078-
if (assignmentDocument === startDocument) {
1105+
if (assignmentDocument === getDocument(startRule)) {
10791106
// the assignment is located in the document of the start rule which is currently validated
10801107
return [<DiagnosticInfo<ast.Assignment | ast.Action>>{
10811108
node: assignment,
@@ -1093,23 +1120,18 @@ export class LangiumGrammarValidator {
10931120
}
10941121
}
10951122

1096-
private checkAssignmentsToTheSameFeatureNested(startRule: ast.ParserRule|ast.Action, nodes: AstNode[], parentMultiplicity: number, map: Map<string, AssignmentUse>, accept: ValidationAcceptor): boolean {
1123+
private checkAssignmentsToTheSameFeatureNested(
1124+
startRule: ast.ParserRule|ast.Action, nodes: AstNode[], parentMultiplicity: number, map: Map<string, AssignmentUse>,
1125+
visiting: ast.ParserRule[], circularFragments: Set<ast.ParserRule>, accept: ValidationAcceptor
1126+
): boolean {
10971127
let resultCreatedNewObject = false;
10981128
// check all given elements
1099-
for (let i = 0; i < nodes.length; i++) {
1100-
const currentNode = nodes[i];
1129+
for (const currentNode of nodes) {
11011130

11021131
// Tree-Rewrite-Actions are a special case: a new object is created => following assignments are put into the new object
11031132
if (ast.isAction(currentNode) && currentNode.feature) {
11041133
// (This does NOT count for unassigned actions, i.e. actions without feature name, since they change only the type of the current object.)
1105-
const mapForNewObject = new Map();
1106-
storeAssignmentUse(mapForNewObject, currentNode.feature, 1, currentNode); // remember the special rewriting feature
1107-
// all following nodes are put into the new object => check their assignments independently
1108-
if (getDocument(currentNode) === getDocument(startRule)) {
1109-
this.checkAssignmentsToTheSameFeatureIndependent(currentNode, nodes.slice(i + 1), accept, mapForNewObject);
1110-
} else {
1111-
// if the rewrite action is inside another document, validate it, when its document is validated (otherwise, it is done twice and validation markers are added to the wrong document)
1112-
}
1134+
// The assignments to AstNodes created by this action are validated by another validation check => nothing to do here
11131135
resultCreatedNewObject = true;
11141136
break; // breaks the current loop
11151137
}
@@ -1130,15 +1152,27 @@ export class LangiumGrammarValidator {
11301152
// Search for assignments in used fragments as well, since their property values are stored in the current object.
11311153
// But do not search in calls of regular parser rules, since parser rules create new objects.
11321154
if (ast.isRuleCall(currentNode) && ast.isParserRule(currentNode.rule.ref) && currentNode.rule.ref.fragment) {
1133-
const createdNewObject = this.checkAssignmentsToTheSameFeatureNested(startRule, [currentNode.rule.ref.definition], currentMultiplicity, map, accept);
1134-
resultCreatedNewObject = createdNewObject || resultCreatedNewObject;
1155+
const foundIndex = visiting.indexOf(currentNode.rule.ref);
1156+
if (foundIndex >= 0) {
1157+
// remember fragments which are called in circular way (their assignments will be counted more often later)
1158+
// all currently visited fragments are part of the circle/path
1159+
for (let i = foundIndex; i < visiting.length; i++) {
1160+
const circleParticipant = visiting[i];
1161+
circularFragments.add(circleParticipant);
1162+
}
1163+
} else {
1164+
visiting.push(currentNode.rule.ref);
1165+
const createdNewObject = this.checkAssignmentsToTheSameFeatureNested(startRule, [currentNode.rule.ref.definition], currentMultiplicity, map, visiting, circularFragments, accept);
1166+
resultCreatedNewObject = createdNewObject || resultCreatedNewObject;
1167+
assertCondition(visiting.pop() === currentNode.rule.ref); // prevent circles, but don't "cache" the results, since the fragment could be called multiple times in non-circular way
1168+
}
11351169
}
11361170

11371171
// look for assignments to the same feature nested within groups
11381172
if (ast.isGroup(currentNode) || ast.isUnorderedGroup(currentNode)) {
11391173
// all members of the group are relavant => collect them all
11401174
const mapGroup: Map<string, AssignmentUse> = new Map(); // store assignments for Alternatives separately
1141-
const createdNewObject = this.checkAssignmentsToTheSameFeatureNested(startRule, currentNode.elements, 1, mapGroup, accept);
1175+
const createdNewObject = this.checkAssignmentsToTheSameFeatureNested(startRule, currentNode.elements, 1, mapGroup, visiting, circularFragments, accept);
11421176
mergeAssignmentUse(mapGroup, map, createdNewObject
11431177
? (s, t) => (s + t) // if a new object is created in the group: ignore the current multiplicity, since a new object is created for each loop cycle!
11441178
: (s, t) => (s * currentMultiplicity + t) // otherwise as usual: take the current multiplicity into account
@@ -1152,7 +1186,7 @@ export class LangiumGrammarValidator {
11521186
let countCreatedObjects = 0;
11531187
for (const alternative of currentNode.elements) {
11541188
const mapCurrentAlternative: Map<string, AssignmentUse> = new Map();
1155-
const createdNewObject = this.checkAssignmentsToTheSameFeatureNested(startRule, [alternative], 1, mapCurrentAlternative, accept);
1189+
const createdNewObject = this.checkAssignmentsToTheSameFeatureNested(startRule, [alternative], 1, mapCurrentAlternative, visiting, circularFragments, accept);
11561190
mergeAssignmentUse(mapCurrentAlternative, mapAllAlternatives, createdNewObject
11571191
? (s, t) => Math.max(s, t) // if a new object is created in an alternative: ignore the current multiplicity, since a new object is created for each loop cycle!
11581192
: (s, t) => Math.max(s * currentMultiplicity, t) // otherwise as usual: take the current multiplicity into account

0 commit comments

Comments
 (0)