@@ -24,8 +24,13 @@ const fileToParse = require('./classes/filesToParse');
2424const luParser = require ( './luParser' ) ;
2525const DiagnosticSeverity = require ( './diagnostic' ) . DiagnosticSeverity ;
2626const BuildDiagnostic = require ( './diagnostic' ) . BuildDiagnostic ;
27- const EntityTypeEnum = require ( './enums/lusiEntityTypes ' ) ;
27+ const EntityTypeEnum = require ( './enums/luisEntityTypes ' ) ;
2828const luisEntityTypeMap = require ( './enums/luisEntityTypeNameMap' ) ;
29+ const plAllowedTypes = [ "simple" , "composite" , "machine-learned" ] ;
30+ const featureTypeEnum = {
31+ featureToModel : 'modelName' ,
32+ modelToFeature : 'featureName'
33+ } ;
2934const INTENTTYPE = 'intent' ;
3035const parseFileContentsModule = {
3136 /**
@@ -148,7 +153,86 @@ const parseLuAndQnaWithAntlr = async function (parsedContent, fileContent, log,
148153 parseFeatureSections ( parsedContent , featuresToProcess ) ;
149154 }
150155}
151-
156+ /**
157+ * Helper function to validate if the requested feature addition is valid.
158+ * @param {String } srcItemType
159+ * @param {String } srcItemName
160+ * @param {String } tgtFeatureType
161+ * @param {String } tgtFeatureName
162+ * @param {String } line
163+ */
164+ const validateFeatureAssignment = function ( srcItemType , srcItemName , tgtFeatureType , tgtFeatureName , line ) {
165+ switch ( srcItemType ) {
166+ case INTENTTYPE :
167+ case EntityTypeEnum . SIMPLE :
168+ case EntityTypeEnum . ML :
169+ case EntityTypeEnum . COMPOSITE :
170+ // can use everything as a feature except pattern.any
171+ if ( tgtFeatureType === EntityTypeEnum . PATTERNANY ) {
172+ let errorMsg = `'patternany' entity cannot be added as a feature. Invalid definition found for "@ ${ srcItemType } ${ srcItemName } usesFeature ${ tgtFeatureName } "` ;
173+ let error = BuildDiagnostic ( {
174+ message : errorMsg ,
175+ context : line
176+ } )
177+ throw ( new exception ( retCode . errorCode . INVALID_INPUT , error . toString ( ) ) ) ;
178+ }
179+ break ;
180+ default :
181+ // cannot have any features assigned
182+ let errorMsg = `Invalid definition found for "@ ${ srcItemType } ${ srcItemName } usesFeature ${ tgtFeatureName } ". usesFeature is only available for intent, ${ plAllowedTypes . join ( ', ' ) } ` ;
183+ let error = BuildDiagnostic ( {
184+ message : errorMsg ,
185+ context : line
186+ } )
187+ throw ( new exception ( retCode . errorCode . INVALID_INPUT , error . toString ( ) ) ) ;
188+ break ;
189+ }
190+ }
191+ /**
192+ * Helper function to add features to the parsed content scope.
193+ * @param {Object } tgtItem
194+ * @param {String } feature
195+ * @param {String } featureType
196+ * @param {Object } line
197+ */
198+ const addFeatures = function ( tgtItem , feature , featureType , line ) {
199+ // target item cannot have the same name as the feature name
200+ if ( tgtItem . name === feature ) {
201+ // Item must be defined before being added as a feature.
202+ let errorMsg = `Source and target cannot be the same for usesFeature. e.g. x usesFeature x is invalid. "${ tgtItem . name } " usesFeature "${ feature } " is invalid.` ;
203+ let error = BuildDiagnostic ( {
204+ message : errorMsg ,
205+ context : line
206+ } )
207+ throw ( new exception ( retCode . errorCode . INVALID_INPUT , error . toString ( ) ) ) ;
208+ }
209+ let featureAlreadyDefined = ( tgtItem . features || [ ] ) . find ( item => item . modelName == feature || item . featureName == feature ) ;
210+ switch ( featureType ) {
211+ case featureTypeEnum . featureToModel : {
212+ if ( tgtItem . features ) {
213+ if ( ! featureAlreadyDefined ) tgtItem . features . push ( new helperClass . featureToModel ( feature ) ) ;
214+ } else {
215+ tgtItem . features = new Array ( new helperClass . featureToModel ( feature ) ) ;
216+ }
217+ break ;
218+ }
219+ case featureTypeEnum . modelToFeature : {
220+ if ( tgtItem . features ) {
221+ if ( ! featureAlreadyDefined ) tgtItem . features . push ( new helperClass . modelToFeature ( feature ) ) ;
222+ } else {
223+ tgtItem . features = new Array ( new helperClass . modelToFeature ( feature ) ) ;
224+ }
225+ break ;
226+ }
227+ default :
228+ break ;
229+ }
230+ }
231+ /**
232+ * Helper function to handle usesFeature definitions
233+ * @param {Object } parsedContent
234+ * @param {Object } featuresToProcess
235+ */
152236const parseFeatureSections = function ( parsedContent , featuresToProcess ) {
153237 // We are only interested in extracting features and setting things up here.
154238 ( featuresToProcess || [ ] ) . forEach ( section => {
@@ -170,21 +254,24 @@ const parseFeatureSections = function(parsedContent, featuresToProcess) {
170254 let featuresList = section . Features . split ( / [ , ; ] / g) . map ( item => item . trim ( ) ) ;
171255 ( featuresList || [ ] ) . forEach ( feature => {
172256 let entityExists = ( parsedContent . LUISJsonStructure . flatListOfEntityAndRoles || [ ] ) . find ( item => item . name == feature || item . name == `${ feature } (interchangeable)` ) ;
257+ let featureIntentExists = ( parsedContent . LUISJsonStructure . intents || [ ] ) . find ( item => item . name == feature ) ;
173258 if ( entityExists ) {
174259 if ( entityExists . type === EntityTypeEnum . PHRASELIST ) {
175260 // de-dupe and add features to intent.
176- if ( intentExists . features ) {
177- let featureAlreadyDefined = ( intentExists . features || [ ] ) . find ( item => item . featureName == feature ) ;
178- if ( ! featureAlreadyDefined ) intentExists . features . push ( new helperClass . intentFeature ( feature ) ) ;
179- } else {
180- intentExists . features = [ new helperClass . intentFeature ( feature ) ] ;
181- }
261+ validateFeatureAssignment ( section . Type , section . Name , entityExists . type , feature , section . ParseTree . newEntityLine ( ) ) ;
262+ addFeatures ( intentExists , feature , featureTypeEnum . featureToModel , section . ParseTree . newEntityLine ( ) ) ;
182263 // set enabledForAllModels on this phrase list
183264 let plEnity = parsedContent . LUISJsonStructure . model_features . find ( item => item . name == feature ) ;
184265 plEnity . enabledForAllModels = false ;
185266 } else {
186267 // de-dupe and add model to intent.
268+ validateFeatureAssignment ( section . Type , section . Name , entityExists . type , feature , section . ParseTree . newEntityLine ( ) ) ;
269+ addFeatures ( intentExists , feature , featureTypeEnum . modelToFeature , section . ParseTree . newEntityLine ( ) ) ;
187270 }
271+ } else if ( featureIntentExists ) {
272+ // Add intent as a feature to another intent
273+ validateFeatureAssignment ( section . Type , section . Name , INTENTTYPE , feature , section . ParseTree . newEntityLine ( ) ) ;
274+ addFeatures ( intentExists , feature , featureTypeEnum . modelToFeature , section . ParseTree . newEntityLine ( ) ) ;
188275 } else {
189276 // Item must be defined before being added as a feature.
190277 let errorMsg = `Features must be defined before assigned to an intent. No definition found for feature "${ feature } " in usesFeature definition for intent "${ section . Name } "` ;
@@ -207,28 +294,31 @@ const parseFeatureSections = function(parsedContent, featuresToProcess) {
207294 // handle as entity
208295 if ( section . Features ) {
209296 let featuresList = section . Features . split ( / [ , ; ] / g) . map ( item => item . trim ( ) ) ;
297+ // Find the source entity from the collection and get its type
298+ let srcEntityInFlatList = ( parsedContent . LUISJsonStructure . flatListOfEntityAndRoles || [ ] ) . find ( item => item . name == section . Name ) ;
299+ let entityType = srcEntityInFlatList ? srcEntityInFlatList . type : undefined ;
210300 ( featuresList || [ ] ) . forEach ( feature => {
211- let entityExists = ( parsedContent . LUISJsonStructure . flatListOfEntityAndRoles || [ ] ) . find ( item => item . name == section . Name ) ;
212- let entityType = undefined ;
213- if ( entityExists ) entityType = entityExists . type ;
214301 let featureExists = ( parsedContent . LUISJsonStructure . flatListOfEntityAndRoles || [ ] ) . find ( item => item . name == feature || item . name == `${ feature } (interchangeable)` ) ;
302+ let featureIntentExists = ( parsedContent . LUISJsonStructure . intents || [ ] ) . find ( item => item . name == feature ) ;
303+ // find the entity based on its type.
304+ let srcEntity = ( parsedContent . LUISJsonStructure [ luisEntityTypeMap [ entityType ] ] || [ ] ) . find ( item => item . name == section . Name ) ;
215305 if ( featureExists ) {
216- // find the entity based on its type.
217- let entityFound = ( parsedContent . LUISJsonStructure [ luisEntityTypeMap [ entityType ] ] || [ ] ) . find ( item => item . name == section . Name ) ;
218306 if ( featureExists . type === EntityTypeEnum . PHRASELIST ) {
219307 // de-dupe and add features to intent.
220- if ( entityFound . features ) {
221- let featureAlreadyDefined = ( entityFound . features || [ ] ) . find ( item => item . featureName == feature ) ;
222- if ( ! featureAlreadyDefined ) entityFound . features . push ( new helperClass . intentFeature ( feature ) ) ;
223- } else {
224- entityFound . features = [ new helperClass . intentFeature ( feature ) ] ;
225- }
308+ validateFeatureAssignment ( entityType , section . Name , featureExists . type , feature , section . ParseTree . newEntityLine ( ) ) ;
309+ addFeatures ( srcEntity , feature , featureTypeEnum . featureToModel , section . ParseTree . newEntityLine ( ) ) ;
226310 // set enabledForAllModels on this phrase list
227311 let plEnity = parsedContent . LUISJsonStructure . model_features . find ( item => item . name == feature ) ;
228312 plEnity . enabledForAllModels = false ;
229313 } else {
230314 // de-dupe and add model to intent.
315+ validateFeatureAssignment ( entityType , section . Name , featureExists . type , feature , section . ParseTree . newEntityLine ( ) ) ;
316+ addFeatures ( srcEntity , feature , featureTypeEnum . modelToFeature , section . ParseTree . newEntityLine ( ) ) ;
231317 }
318+ } else if ( featureIntentExists ) {
319+ // Add intent as a feature to another intent
320+ validateFeatureAssignment ( entityType , section . Name , INTENTTYPE , feature , section . ParseTree . newEntityLine ( ) ) ;
321+ addFeatures ( srcEntity , feature , featureTypeEnum . modelToFeature , section . ParseTree . newEntityLine ( ) ) ;
232322 } else {
233323 // Item must be defined before being added as a feature.
234324 let errorMsg = `Features must be defined before assigned to an entity. No definition found for feature "${ feature } " in usesFeature definition for entity "${ section . Name } "` ;
@@ -241,8 +331,76 @@ const parseFeatureSections = function(parsedContent, featuresToProcess) {
241331 } ) ;
242332 }
243333 }
244- } )
334+ } ) ;
335+
336+ // Circular dependency for features is not allowed. E.g. A usesFeature B usesFeature A is not valid.
337+ verifyNoCircularDependencyForFeatures ( parsedContent ) ;
245338}
339+
340+ /**
341+ * Helper function to update a list of dependencies for usesFeature
342+ * @param {String } type
343+ * @param {Object } parsedContent
344+ * @param {Object } dependencyList
345+ */
346+ const updateDependencyList = function ( type , parsedContent , dependencyList ) {
347+ // go through intents and capture dependency list
348+ ( parsedContent . LUISJsonStructure [ type ] || [ ] ) . forEach ( itemOfType => {
349+ let srcName = itemOfType . name ;
350+ let copySrc , copyValue ;
351+ if ( itemOfType . features ) {
352+ ( itemOfType . features || [ ] ) . forEach ( feature => {
353+ if ( feature . modelName ) feature = feature . modelName ;
354+ if ( feature . featureName ) feature = feature . featureName ;
355+ // find any items where this feature is the target
356+ let featureDependencyEx = dependencyList . filter ( item => srcName == ( item . value ? item . value . slice ( - 1 ) [ 0 ] : undefined ) ) ;
357+ ( featureDependencyEx || [ ] ) . forEach ( item => {
358+ item . key = `${ item . key . split ( '::' ) [ 0 ] } ::${ feature } ` ;
359+ item . value . push ( feature ) ;
360+ } )
361+ // find any items where this feature is the source
362+ featureDependencyEx = dependencyList . find ( item => feature == ( item . value ? item . value . slice ( 0 ) [ 0 ] : undefined ) ) ;
363+ if ( featureDependencyEx ) {
364+ copySrc = featureDependencyEx . key . split ( '::' ) [ 1 ] ;
365+ copyValue = featureDependencyEx . value . slice ( 1 ) ;
366+ }
367+ let dependencyExists = dependencyList . find ( item => item . key == `${ srcName } ::${ feature } ` ) ;
368+ if ( ! dependencyExists ) {
369+ let lKey = copySrc ? `${ srcName } ::${ copySrc } ` : `${ srcName } ::${ feature } ` ;
370+ let lValue = [ srcName , feature ] ;
371+ if ( copyValue ) copyValue . forEach ( item => lValue . push ( item ) ) ;
372+ dependencyList . push ( {
373+ key : lKey ,
374+ value : lValue
375+ } )
376+ } else {
377+ dependencyExists . key = `${ dependencyExists . key . split ( '::' ) [ 0 ] } ::${ feature } ` ;
378+ dependencyExists . value . push ( feature ) ;
379+ }
380+ let circularItemFound = dependencyList . find ( item => item . value && item . value . slice ( 0 ) [ 0 ] == item . value . slice ( - 1 ) [ 0 ] ) ;
381+ if ( circularItemFound ) {
382+ throw ( new exception ( retCode . errorCode . INVALID_INPUT , `Circular dependency found for usesFeature. ${ circularItemFound . value . join ( ' -> ' ) } ` ) ) ;
383+ }
384+
385+ } )
386+ }
387+ } ) ;
388+ }
389+ /**
390+ * Helper function to verify there are no circular dependencies in the parsed content.
391+ * @param {Object } parsedContent
392+ */
393+ const verifyNoCircularDependencyForFeatures = function ( parsedContent ) {
394+ let dependencyList = [ ] ;
395+ updateDependencyList ( LUISObjNameEnum . INTENT , parsedContent , dependencyList ) ;
396+ updateDependencyList ( LUISObjNameEnum . ENTITIES , parsedContent , dependencyList ) ;
397+ updateDependencyList ( LUISObjNameEnum . CLOSEDLISTS , parsedContent , dependencyList ) ;
398+ updateDependencyList ( LUISObjNameEnum . COMPOSITES , parsedContent , dependencyList ) ;
399+ updateDependencyList ( LUISObjNameEnum . PATTERNANYENTITY , parsedContent , dependencyList ) ;
400+ updateDependencyList ( LUISObjNameEnum . PREBUILT , parsedContent , dependencyList ) ;
401+ updateDependencyList ( LUISObjNameEnum . REGEX , parsedContent , dependencyList ) ;
402+ }
403+
246404/**
247405 * Reference parser code to parse reference section.
248406 * @param {parserObj } Object with that contains list of additional files to parse, parsed LUIS object and parsed QnA object
0 commit comments