@@ -58,7 +58,7 @@ export function registerValidationChecks(services: LangiumGrammarServices): void
5858 validator . checkRuleParameters ,
5959 validator . checkEmptyParserRule ,
6060 validator . checkParserRuleReservedName ,
61- validator . checkOperatorMultiplicitiesForMultiAssignments ,
61+ validator . checkAssignmentsToTheSameFeature ,
6262 ] ,
6363 InfixRule : [
6464 validator . checkInfixRuleDataType ,
@@ -139,6 +139,7 @@ export namespace IssueCodes {
139139 export const OptionalUnorderedGroup = 'optional-unordered-group' ;
140140 export const ParsingRuleEmpty = 'parsing-rule-empty' ;
141141 export const ReplaceOperatorMultiAssignment = 'replace-operator-for-multi-assignments' ;
142+ export const MixedAssignmentOperators = 'mixed-use-of-assignment-operators' ;
142143}
143144
144145export class LangiumGrammarValidator {
@@ -1018,22 +1019,45 @@ export class LangiumGrammarValidator {
10181019 }
10191020 }
10201021
1021- /** This validation recursively looks at all assignments (and rewriting actions) with '=' as assignment operator and checks,
1022- * whether the operator should be '+=' instead. */
1023- checkOperatorMultiplicitiesForMultiAssignments ( rule : ast . ParserRule , accept : ValidationAcceptor ) : void {
1022+ /**
1023+ * This validation recursively collects all assignments (and rewriting actions) to the same feature and validates them:
1024+ * Assignment operators '?=', '=' and '+=' should not be mixed.
1025+ * Assignments with '=' as assignment operator should be replaced be '+=', if assignments to the feature might occure more than once.
1026+ */
1027+ checkAssignmentsToTheSameFeature ( rule : ast . ParserRule , accept : ValidationAcceptor ) : void {
10241028 // for usual parser rules AND for fragments, but not for data type rules!
10251029 if ( ! rule . dataType ) {
1026- this . checkOperatorMultiplicitiesForMultiAssignmentsIndependent ( [ rule . definition ] , accept ) ;
1030+ this . checkAssignmentsToTheSameFeatureIndependent ( [ rule . definition ] , accept ) ;
10271031 }
10281032 }
10291033
1030- private checkOperatorMultiplicitiesForMultiAssignmentsIndependent ( startNodes : AstNode [ ] , accept : ValidationAcceptor , map : Map < string , AssignmentUse > = new Map ( ) ) : void {
1034+ private checkAssignmentsToTheSameFeatureIndependent ( startNodes : AstNode [ ] , accept : ValidationAcceptor , map : Map < string , AssignmentUse > = new Map ( ) ) : void {
10311035 // check all starting nodes
1032- this . checkOperatorMultiplicitiesForMultiAssignmentsNested ( startNodes , 1 , map , accept ) ;
1036+ this . checkAssignmentsToTheSameFeatureNested ( startNodes , 1 , map , accept ) ;
10331037
10341038 // create the warnings
10351039 for ( const entry of map . values ( ) ) {
1036- if ( entry . counter >= 2 ) {
1040+ // check mixed use of ?=, = and +=
1041+ const usedOperators = new Set < string > ( ) ;
1042+ for ( const assignment of entry . assignments ) {
1043+ if ( assignment . operator ) {
1044+ usedOperators . add ( assignment . operator ) ;
1045+ }
1046+ }
1047+ if ( usedOperators . size >= 2 ) {
1048+ for ( const assignment of entry . assignments ) {
1049+ accept (
1050+ 'warning' ,
1051+ `Don't mix operators (${ stream ( usedOperators ) . map ( op => `'${ op } '` ) . join ( ', ' ) } ) when assigning values to the same feature '${ assignment . feature } '.` ,
1052+ {
1053+ node : assignment ,
1054+ property : 'feature' , // use 'feature' instead of 'operator', since it is pretty hard to see
1055+ data : diagnosticData ( IssueCodes . MixedAssignmentOperators ) , // no code action, but is relevant for serializability
1056+ }
1057+ ) ;
1058+ }
1059+ } else if ( entry . counter >= 2 ) {
1060+ // check for multiple assignments with '?=' or '=' instead of '+='
10371061 for ( const assignment of entry . assignments ) {
10381062 if ( assignment . operator !== '+=' ) {
10391063 accept (
@@ -1042,16 +1066,18 @@ export class LangiumGrammarValidator {
10421066 {
10431067 node : assignment ,
10441068 property : 'feature' , // use 'feature' instead of 'operator', since it is pretty hard to see
1045- data : diagnosticData ( IssueCodes . ReplaceOperatorMultiAssignment ) ,
1069+ data : diagnosticData ( IssueCodes . ReplaceOperatorMultiAssignment ) , // for code action, is relevant for serializability as well
10461070 }
10471071 ) ;
10481072 }
10491073 }
1074+ } else {
1075+ // the assignments to this feature are fine
10501076 }
10511077 }
10521078 }
10531079
1054- private checkOperatorMultiplicitiesForMultiAssignmentsNested ( nodes : AstNode [ ] , parentMultiplicity : number , map : Map < string , AssignmentUse > , accept : ValidationAcceptor ) : boolean {
1080+ private checkAssignmentsToTheSameFeatureNested ( nodes : AstNode [ ] , parentMultiplicity : number , map : Map < string , AssignmentUse > , accept : ValidationAcceptor ) : boolean {
10551081 let resultCreatedNewObject = false ;
10561082 // check all given elements
10571083 for ( let i = 0 ; i < nodes . length ; i ++ ) {
@@ -1063,7 +1089,7 @@ export class LangiumGrammarValidator {
10631089 const mapForNewObject = new Map ( ) ;
10641090 storeAssignmentUse ( mapForNewObject , currentNode . feature , 1 , currentNode ) ; // remember the special rewriting feature
10651091 // all following nodes are put into the new object => check their assignments independently
1066- this . checkOperatorMultiplicitiesForMultiAssignmentsIndependent ( nodes . slice ( i + 1 ) , accept , mapForNewObject ) ;
1092+ this . checkAssignmentsToTheSameFeatureIndependent ( nodes . slice ( i + 1 ) , accept , mapForNewObject ) ;
10671093 resultCreatedNewObject = true ;
10681094 break ; // breaks the current loop
10691095 }
@@ -1084,15 +1110,15 @@ export class LangiumGrammarValidator {
10841110 // Search for assignments in used fragments as well, since their property values are stored in the current object.
10851111 // But do not search in calls of regular parser rules, since parser rules create new objects.
10861112 if ( ast . isRuleCall ( currentNode ) && ast . isParserRule ( currentNode . rule . ref ) && currentNode . rule . ref . fragment ) {
1087- const createdNewObject = this . checkOperatorMultiplicitiesForMultiAssignmentsNested ( [ currentNode . rule . ref . definition ] , currentMultiplicity , map , accept ) ;
1113+ const createdNewObject = this . checkAssignmentsToTheSameFeatureNested ( [ currentNode . rule . ref . definition ] , currentMultiplicity , map , accept ) ;
10881114 resultCreatedNewObject = createdNewObject || resultCreatedNewObject ;
10891115 }
10901116
10911117 // look for assignments to the same feature nested within groups
10921118 if ( ast . isGroup ( currentNode ) || ast . isUnorderedGroup ( currentNode ) ) {
10931119 // all members of the group are relavant => collect them all
10941120 const mapGroup : Map < string , AssignmentUse > = new Map ( ) ; // store assignments for Alternatives separately
1095- const createdNewObject = this . checkOperatorMultiplicitiesForMultiAssignmentsNested ( currentNode . elements , 1 , mapGroup , accept ) ;
1121+ const createdNewObject = this . checkAssignmentsToTheSameFeatureNested ( currentNode . elements , 1 , mapGroup , accept ) ;
10961122 mergeAssignmentUse ( mapGroup , map , createdNewObject
10971123 ? ( 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!
10981124 : ( s , t ) => ( s * currentMultiplicity + t ) // otherwise as usual: take the current multiplicity into account
@@ -1106,7 +1132,7 @@ export class LangiumGrammarValidator {
11061132 let countCreatedObjects = 0 ;
11071133 for ( const alternative of currentNode . elements ) {
11081134 const mapCurrentAlternative : Map < string , AssignmentUse > = new Map ( ) ;
1109- const createdNewObject = this . checkOperatorMultiplicitiesForMultiAssignmentsNested ( [ alternative ] , 1 , mapCurrentAlternative , accept ) ;
1135+ const createdNewObject = this . checkAssignmentsToTheSameFeatureNested ( [ alternative ] , 1 , mapCurrentAlternative , accept ) ;
11101136 mergeAssignmentUse ( mapCurrentAlternative , mapAllAlternatives , createdNewObject
11111137 ? ( 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!
11121138 : ( s , t ) => Math . max ( s * currentMultiplicity , t ) // otherwise as usual: take the current multiplicity into account
0 commit comments