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