44 * terms of the MIT License, which is available in the project root.
55 ******************************************************************************/
66
7+ import type { Range } from 'vscode-languageserver-types' ;
8+ import { DiagnosticTag } from 'vscode-languageserver-types' ;
9+ import * as ast from '../../languages/generated/ast.js' ;
710import type { NamedAstNode } from '../../references/name-provider.js' ;
811import type { References } from '../../references/references.js' ;
912import type { AstNode , Properties , Reference } from '../../syntax-tree.js' ;
10- import type { Stream } from '../../utils/stream.js' ;
11- import type { DiagnosticData , ValidationAcceptor , ValidationChecks } from '../../validation/validation-registry.js' ;
12- import type { LangiumDocuments } from '../../workspace/documents.js' ;
13- import type { LangiumGrammarServices } from '../langium-grammar-module.js' ;
14- import type { Range } from 'vscode-languageserver-types' ;
15- import { DiagnosticTag } from 'vscode-languageserver-types' ;
1613import { getContainerOfType , streamAllContents } from '../../utils/ast-utils.js' ;
1714import { MultiMap } from '../../utils/collections.js' ;
1815import { toDocumentSegment } from '../../utils/cst-utils.js' ;
19- import { findNameAssignment , findNodeForKeyword , findNodeForProperty , getAllReachableRules , isDataTypeRule , isOptionalCardinality , terminalRegex } from '../../utils/grammar-utils.js' ;
16+ import { findNameAssignment , findNodeForKeyword , findNodeForProperty , getAllReachableRules , isArrayCardinality , isDataTypeRule , isOptionalCardinality , terminalRegex } from '../../utils/grammar-utils.js' ;
17+ import type { Stream } from '../../utils/stream.js' ;
2018import { stream } from '../../utils/stream.js' ;
19+ import type { DiagnosticData , ValidationAcceptor , ValidationChecks } from '../../validation/validation-registry.js' ;
2120import { diagnosticData } from '../../validation/validation-registry.js' ;
22- import * as ast from '../../languages/generated/ast.js' ;
21+ import type { AstNodeLocator } from '../../workspace/ast-node-locator.js' ;
22+ import type { LangiumDocuments } from '../../workspace/documents.js' ;
2323import { getTypeNameWithoutError , hasDataTypeReturn , isPrimitiveGrammarType , isStringGrammarType , resolveImport , resolveTransitiveImports } from '../internal-grammar-util.js' ;
24+ import type { LangiumGrammarServices } from '../langium-grammar-module.js' ;
2425import { typeDefinitionToPropertyType } from '../type-system/type-collector/declared-types.js' ;
2526import { flattenPlainType , isPlainReferenceType } from '../type-system/type-collector/plain-types.js' ;
2627
@@ -43,6 +44,7 @@ export function registerValidationChecks(services: LangiumGrammarServices): void
4344 validator . checkRuleParametersUsed ,
4445 validator . checkEmptyParserRule ,
4546 validator . checkParserRuleReservedName ,
47+ validator . checkOperatorMultiplicitiesForMultiAssignments ,
4648 ] ,
4749 TerminalRule : [
4850 validator . checkTerminalRuleReturnType ,
@@ -78,7 +80,7 @@ export function registerValidationChecks(services: LangiumGrammarServices): void
7880 validator . checkUsedHiddenTerminalRule ,
7981 validator . checkUsedFragmentTerminalRule ,
8082 validator . checkRuleCallParameters ,
81- validator . checkRuleCallMultiplicity
83+ validator . checkMultiRuleCallsAreAssigned
8284 ] ,
8385 TerminalRuleCall : validator . checkUsedHiddenTerminalRule ,
8486 CrossReference : [
@@ -118,10 +120,12 @@ export namespace IssueCodes {
118120export class LangiumGrammarValidator {
119121
120122 protected readonly references : References ;
123+ protected readonly nodeLocator : AstNodeLocator ;
121124 protected readonly documents : LangiumDocuments ;
122125
123126 constructor ( services : LangiumGrammarServices ) {
124127 this . references = services . references . References ;
128+ this . nodeLocator = services . workspace . AstNodeLocator ;
125129 this . documents = services . shared . workspace . LangiumDocuments ;
126130 }
127131
@@ -722,7 +726,8 @@ export class LangiumGrammarValidator {
722726 }
723727 }
724728
725- checkRuleCallMultiplicity ( call : ast . RuleCall , accept : ValidationAcceptor ) : void {
729+ /** This validation checks, that parser rules which are called multiple times are assigned (except for fragments). */
730+ checkMultiRuleCallsAreAssigned ( call : ast . RuleCall , accept : ValidationAcceptor ) : void {
726731 const findContainerWithCardinality = ( node : AstNode ) => {
727732 let result : AstNode | undefined = node ;
728733 while ( result !== undefined ) {
@@ -867,6 +872,108 @@ export class LangiumGrammarValidator {
867872 }
868873 }
869874
875+ /** This validation recursively looks at all assignments (and rewriting actions) with '=' as assignment operator and checks,
876+ * whether the operator should be '+=' instead. */
877+ checkOperatorMultiplicitiesForMultiAssignments ( rule : ast . ParserRule , accept : ValidationAcceptor ) : void {
878+ // for usual parser rules AND for fragments, but not for data type rules!
879+ if ( ! rule . dataType ) {
880+ this . checkOperatorMultiplicitiesForMultiAssignmentsLogic ( [ rule . definition ] , accept ) ;
881+ }
882+ }
883+
884+ private checkOperatorMultiplicitiesForMultiAssignmentsLogic ( startNodes : AstNode [ ] , accept : ValidationAcceptor ) : void {
885+ // new map to store usage information of the assignments
886+ const map : Map < string , AssignmentUse > = new Map ( ) ;
887+
888+ // top-down traversal for all starting nodes
889+ for ( const node of startNodes ) {
890+ this . checkAssignmentNumbersForNode ( node , 1 , map , accept ) ;
891+ }
892+
893+ // create the warnings
894+ for ( const entry of map . values ( ) ) {
895+ if ( entry . counter >= 2 ) {
896+ for ( const assignment of entry . assignments ) {
897+ if ( assignment . operator !== '+=' ) {
898+ accept (
899+ 'warning' ,
900+ `Found multiple assignments to '${ assignment . feature } ' with the '${ assignment . operator } ' assignment operator. Consider using '+=' instead to prevent data loss.` ,
901+ { node : assignment , property : 'feature' } // use 'feature' instead of 'operator', since it is pretty hard to see
902+ ) ;
903+ }
904+ }
905+ }
906+ }
907+ }
908+
909+ private checkAssignmentNumbersForNode ( currentNode : AstNode , parentMultiplicity : number , map : Map < string , AssignmentUse > , accept : ValidationAcceptor ) {
910+ // the current element can occur multiple times => its assignments can occur multiple times as well
911+ let currentMultiplicity = parentMultiplicity ;
912+ if ( ast . isAbstractElement ( currentNode ) && isArrayCardinality ( currentNode . cardinality ) ) {
913+ currentMultiplicity *= 2 ; // note, that the result is not exact (but it is sufficient for the current case)!
914+ }
915+
916+ // assignment
917+ if ( ast . isAssignment ( currentNode ) ) {
918+ storeAssignmentUse ( map , currentNode . feature , 1 * currentMultiplicity , currentNode ) ;
919+ }
920+
921+ // Search for assignments in used fragments as well, since their property values are stored in the current object.
922+ // But do not search in calls of regular parser rules, since parser rules create new objects.
923+ if ( ast . isRuleCall ( currentNode ) && ast . isParserRule ( currentNode . rule . ref ) && currentNode . rule . ref . fragment ) {
924+ this . checkAssignmentNumbersForNode ( currentNode . rule . ref . definition , currentMultiplicity , map , accept ) ;
925+ }
926+
927+ // rewriting actions are a special case for assignments
928+ if ( ast . isAction ( currentNode ) && currentNode . feature ) {
929+ storeAssignmentUse ( map , currentNode . feature , 1 * currentMultiplicity , currentNode ) ;
930+ }
931+
932+ // look for assignments to the same feature nested within groups
933+ if ( ast . isGroup ( currentNode ) || ast . isUnorderedGroup ( currentNode ) || ast . isAlternatives ( currentNode ) ) {
934+ const mapAllAlternatives : Map < string , AssignmentUse > = new Map ( ) ; // store assignments for Alternatives separately
935+ let nodesForNewObject : AstNode [ ] = [ ] ;
936+ // check all elements inside the current group
937+ for ( const child of currentNode . elements ) {
938+ if ( ast . isAction ( child ) ) {
939+ // Actions are a special case: a new object is created => following assignments are put into the new object
940+ // (This counts for rewriting actions as well as for unassigned actions, i.e. actions without feature name)
941+ if ( nodesForNewObject . length > 0 ) {
942+ // all collected nodes are put into the new object => check their assignments independently
943+ this . checkOperatorMultiplicitiesForMultiAssignmentsLogic ( nodesForNewObject , accept ) ;
944+ // is it possible to have two or more Actions within the same parser rule? the grammar allows that ...
945+ nodesForNewObject = [ ] ;
946+ }
947+ // push the current node into a new object
948+ nodesForNewObject . push ( child ) ;
949+ } else {
950+ // for non-Actions
951+ if ( nodesForNewObject . length > 0 ) {
952+ // nodes go into a new object
953+ nodesForNewObject . push ( child ) ;
954+ } else {
955+ // count the relevant child assignments
956+ if ( ast . isAlternatives ( currentNode ) ) {
957+ // for alternatives, only a single alternative is used => assume the worst case and take the maximum number of assignments
958+ const mapCurrentAlternative : Map < string , AssignmentUse > = new Map ( ) ;
959+ this . checkAssignmentNumbersForNode ( child , currentMultiplicity , mapCurrentAlternative , accept ) ;
960+ mergeAssignmentUse ( mapCurrentAlternative , mapAllAlternatives , ( s , t ) => Math . max ( s , t ) ) ;
961+ } else {
962+ // all members of the group are relavant => collect them all
963+ this . checkAssignmentNumbersForNode ( child , currentMultiplicity , map , accept ) ;
964+ }
965+ }
966+ }
967+ }
968+ // merge alternatives
969+ mergeAssignmentUse ( mapAllAlternatives , map ) ;
970+ if ( nodesForNewObject . length >= 1 ) {
971+ // these nodes are put into a new object => check their assignments independently
972+ this . checkOperatorMultiplicitiesForMultiAssignmentsLogic ( nodesForNewObject , accept ) ;
973+ }
974+ }
975+ }
976+
870977 checkInterfacePropertyTypes ( interfaceDecl : ast . Interface , accept : ValidationAcceptor ) : void {
871978 for ( const attribute of interfaceDecl . attributes ) {
872979 if ( attribute . type ) {
@@ -1095,3 +1202,48 @@ function findLookAheadGroup(rule: AstNode | undefined): ast.TerminalGroup | unde
10951202 return findLookAheadGroup ( terminalGroup . $container ) ;
10961203 }
10971204}
1205+
1206+ /*
1207+ * Internal helper stuff for collecting information about assignments to features and their cardinalities
1208+ */
1209+
1210+ interface AssignmentUse {
1211+ /**
1212+ * Collects assignments for the same feature, while an Action represents a "special assignment", when it is a rewrite action.
1213+ * The Set is used in order not to store the same assignment multiple times.
1214+ */
1215+ assignments : Set < ast . Assignment | ast . Action > ;
1216+ /**
1217+ * Note, that this number is not exact and "estimates the potential number",
1218+ * i.e. multiplicities like + and * are counted as 2x/twice,
1219+ * and for alternatives, the worst case is assumed.
1220+ * In other words, here it is enough to know, whether there are two or more assignments possible to the same feature.
1221+ */
1222+ counter : number ;
1223+ }
1224+
1225+ function storeAssignmentUse ( map : Map < string , AssignmentUse > , feature : string , increment : number , ...assignments : Array < ast . Assignment | ast . Action > ) {
1226+ let entry = map . get ( feature ) ;
1227+ if ( ! entry ) {
1228+ entry = {
1229+ assignments : new Set ( ) ,
1230+ counter : 0 ,
1231+ } ;
1232+ map . set ( feature , entry ) ;
1233+ }
1234+ assignments . forEach ( a => entry ! . assignments . add ( a ) ) ; // a Set is necessary, since assignments in Fragements might be used multiple times by different parser rules, but they should be marked only once!
1235+ entry . counter += increment ;
1236+ }
1237+
1238+ function mergeAssignmentUse ( mapSoure : Map < string , AssignmentUse > , mapTarget : Map < string , AssignmentUse > , counterOperation : ( s : number , t : number ) => number = ( s , t ) => s + t ) : void {
1239+ for ( const [ key , source ] of mapSoure . entries ( ) ) {
1240+ const target = mapTarget . get ( key ) ;
1241+ if ( target ) {
1242+ source . assignments . forEach ( a => target . assignments . add ( a ) ) ;
1243+ target . counter = counterOperation ( source . counter , target . counter ) ;
1244+ } else {
1245+ mapTarget . set ( key , source ) ;
1246+ }
1247+ }
1248+ mapSoure . clear ( ) ;
1249+ }
0 commit comments