@@ -13,7 +13,7 @@ import type { AstNode, Properties, Reference } from '../../syntax-tree.js';
1313import { getContainerOfType , getDocument , streamAllContents } from '../../utils/ast-utils.js' ;
1414import { MultiMap } from '../../utils/collections.js' ;
1515import { toDocumentSegment } from '../../utils/cst-utils.js' ;
16- import { assertUnreachable } from '../../utils/errors.js' ;
16+ import { assertCondition , assertUnreachable } from '../../utils/errors.js' ;
1717import { findNameAssignment , findNodeForKeyword , findNodeForProperty , getAllReachableRules , getAllRulesUsedForCrossReferences , isArrayCardinality , isDataTypeRule , isOptionalCardinality , terminalRegex } from '../../utils/grammar-utils.js' ;
1818import type { Stream } from '../../utils/stream.js' ;
1919import { 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