import { templateKeys } from "../../../common" ; import { assert } from "../../fixed_assert" ; import { getFriendlyExpressionFromTleExpression } from "../../language/expressions/friendlyExpressions" ; import * as TLE from "../../language/expressions/TLE" ; import * as Json from "../../language/json/JSON" ; import { isSingleQuoted , removeSingleQuotes } from "../../util/strings" ; import { areDecoupledChildAndParent } from "./areDecoupledChildAndParent" ; import { TemplateScope } from "./scopes/TemplateScope" ; /**
 * Get useful info about each resource in the template in a flat list, including the ability to understand full resource names and types in the
 * presence of parent/child resources
 */ export function getResourcesInfo ( { scope , recognizeDecoupledChildren } ) { const resourcesArray = scope . rootObject ?. getPropertyValue ( templateKeys . resources ) ?. asArrayValue ; if ( resourcesArray ) { const infos = getInfoFromResourcesArray ( resourcesArray , undefined ) ; if ( recognizeDecoupledChildren ) { findAndSetDecoupledChildren ( infos ) ; } return infos ; } return [ ] ; } /**
 * Get useful info about a single resource in a template, ignoring children and parents
 */ export function getResourceInfo ( resourceObject ) { const results = [ ] ; getInfoFromResourceObject ( resourceObject , undefined , results , false ) ; return results [ 0 ] ; } export interface IResourceInfo { /**
     * The parent resource, if any (might be a decoupled parent)
     */ parent ? : IResourceInfo ; /**
     * True if this is a child that was defined at top level instead of nested inside the parent
     */ isDecoupledChild : boolean ; children : IResourceInfo [ ] ; /**
     * The full reource name, split into segments (e.g. "'sqlServer/firewallRule1'" => ["'sqlServer'", "'firewallRule1'"]).
     * For nested child resources, the name of the parent will automatically be added if not specified in the child
     */ nameSegmentExpressions : string [ ] ; /**
     * The full reource type as a TLE expression, split into segments (e.g. "'Microsoft.sql/servers/firewallRules'" => ["'Microsoft.sql/servers'", "'firewallRules'"]).
     * For nested child resources, the type of the parent will automatically be added if not specified in the child
     */ typeSegmentExpressions : string [ ] ; example example shortNameExpression : string | undefined ; example example shortTypeExpression : string | undefined ; example getFullNameExpression ( ) : string | undefined ; example getFullTypeExpression ( ) : string | undefined ; /**
     * Creates a resourceId expression to reference this resource
     */ getResourceIdExpression ( ) : string | undefined ; /**
     * Retrieves a shortened, user-friendly label for the resource that can be used in treeviews,
     * code lens references, etc.  It consists of a version of the name (short or full) and type, using
     * a format similar to the new string interpolation (e.g. concat's are coalesced and var/param references
     * use ${x} notation).
     */ getFriendlyResourceLabel ( { fullName } ) : string ; /**
     * Retrieves a friendly name expression (might be the display name) for just the resource name
     */ getFriendlyNameExpression ( { fullName } ) : string ; /**
     * Retrieves a friendly name expression for just the resource type
     */ getFriendlyTypeExpression ( { fullType } ) : string ; } export interface IJsonResourceInfo extends IResourceInfo { /**
     * The JSON object that represents this resource
     */ resourceObject : Json . ObjectValue ; /**
     * The COPY element for this resource, if any
     */ copyBlockElement : Json . ObjectValue | undefined ; } export class ResourceInfo implements IResourceInfo { public readonly children : IResourceInfo [ ] = [ ] ; public constructor ( public readonly nameSegmentExpressions , public readonly typeSegmentExpressions , public readonly parent ? ) { if ( parent ) { parent . children . push ( this ) ; } } public isDecoupledChild : boolean = false ; public get shortNameExpression ( ) : string { return this . nameSegmentExpressions [ this . nameSegmentExpressions . length - 1 ] ; } public get shortTypeExpression ( ) : string | undefined { if ( this . typeSegmentExpressions . length > 1 ) { return this . typeSegmentExpressions [ this . typeSegmentExpressions . length - 1 ] ; } else { const firstSegment = this . typeSegmentExpressions [ 0 ] ; if ( firstSegment && isSingleQuoted ( firstSegment ) ) { return firstSegment . replace ( /'.+\//g , "'" ) ; } else { return firstSegment ; } } } public getFullTypeExpression ( ) { return concatExpressionsWithSeparator ( this . typeSegmentExpressions , '/' ) ; } public getFullNameExpression ( ) { return concatExpressionsWithSeparator ( this . nameSegmentExpressions , '/' ) ; } public getResourceIdExpression ( ) { if ( this . nameSegmentExpressions . length > 0 && this . typeSegmentExpressions . length > 0 ) { const typeExpression = this . getFullTypeExpression ( ) ; const nameExpressions = this . nameSegmentExpressions . join ( ', ' ) ; return `resourceId(${ typeExpression }, ${ nameExpressions })` ; } return undefined ; } public getFriendlyNameExpression ( { fullName } ) { return getFriendlyNameExpression ( { resource : this , fullName } ) ; } public getFriendlyTypeExpression ( { fullType } ) { return getFriendlyTypeExpression ( { resource : this , fullType } ) ; } public getFriendlyResourceLabel ( { fullName , fullType } ) { return getFriendlyResourceLabel ( { resource : this , fullName , fullType } ) ; } } export class JsonResourceInfo extends ResourceInfo implements IJsonResourceInfo { public constructor ( nameSegmentExpressions , typeSegmentExpressions , public readonly resourceObject , parent ) { super ( nameSegmentExpressions , typeSegmentExpressions , parent ) ; } public get copyBlockElement ( ) : Json . ObjectValue | undefined { return this . resourceObject . getPropertyValue ( templateKeys . copyLoop ) ?. asObjectValue ; } } param expressions param unquotedLiteralSeparator example function concatExpressionsWithSeparator ( expressions , unquotedLiteralSeparator ? ) { if ( expressions . length < 2 ) { return expressions [ 0 ] ; } const coalescedExpressions = [ ] ; const expressionsLength = expressions . length ; for ( let i = 0 ; i < expressionsLength ; ++ i ) { let expression = expressions [ i ] ; const isLastSegment = i === expressionsLength - 1 ; if ( isSingleQuoted ( expression ) ) { if ( unquotedLiteralSeparator && ! isLastSegment ) { expression = `'${ removeSingleQuotes ( expression ) }${ unquotedLiteralSeparator }'` ; } const lastCoalescedExpression = coalescedExpressions [ coalescedExpressions . length - 1 ] ; if ( lastCoalescedExpression && isSingleQuoted ( lastCoalescedExpression ) ) { coalescedExpressions [ coalescedExpressions . length - 1 ] = `'${ removeSingleQuotes ( lastCoalescedExpression ) }${ removeSingleQuotes ( expression ) }'` ; } else { coalescedExpressions . push ( expression ) ; } } else { coalescedExpressions . push ( expression ) ; if ( unquotedLiteralSeparator && ! isLastSegment ) { coalescedExpressions . push ( `'${ unquotedLiteralSeparator }'` ) ; } } } if ( coalescedExpressions . length < 2 ) { return coalescedExpressions [ 0 ] ; } return `concat(${ coalescedExpressions . join ( ', ' ) })` ; } function getInfoFromResourcesArray ( resourcesArray , parent ) { const results = [ ] ; for ( let resourceValue of resourcesArray . elements ?? [ ] ) { const resourceObject = Json . asObjectValue ( resourceValue ) ; if ( resourceObject ) { getInfoFromResourceObject ( resourceObject , parent , results , true ) ; } } return results ; } function getInfoFromResourceObject ( resourceObject , parent , results , processChildren ) { const resName = Json . asStringValue ( resourceObject . getPropertyValue ( templateKeys . resourceName ) ) ; const resType = Json . asStringValue ( resourceObject . getPropertyValue ( templateKeys . resourceType ) ) ; if ( resName && resType ) { let nameExpressions ; let typeExpressions ; const typeSegments = splitResourceTypeIntoSegments ( resType . unquotedValue ) ; if ( parent && typeSegments . length <= 1 ) { typeExpressions = [ ... parent . typeSegmentExpressions , ... typeSegments ] ; } else { typeExpressions = typeSegments ; } const nameSegments = splitResourceNameIntoSegments ( resName . unquotedValue ) ; if ( parent && nameSegments . length <= 1 ) { nameExpressions = [ ... parent . nameSegmentExpressions , ... nameSegments ] ; } else { nameExpressions = nameSegments ; } const info = new JsonResourceInfo ( nameExpressions , typeExpressions , resourceObject , parent ) ; results . push ( info ) ; if ( processChildren ) { const childResources = resourceObject . getPropertyValue ( templateKeys . resources ) ?. asArrayValue ; if ( childResources ) { const childrenInfo = getInfoFromResourcesArray ( childResources , info ) ; results . push ( ... childrenInfo ) ; } if ( resType . unquotedValue . toLowerCase ( ) === 'microsoft.network/virtualnetworks' ) { const subnets = resourceObject . getPropertyValue ( templateKeys . properties ) ?. asObjectValue ?. getPropertyValue ( 'subnets' ) ?. asArrayValue ; for ( let subnet of subnets ?. elements ?? [ ] ) { const subnetObject = subnet . asObjectValue ; const subnetName = subnetObject ?. getPropertyValue ( templateKeys . resourceName ) ?. asStringValue ; if ( subnetObject && subnetName ) { const subnetTypes = [ `'Microsoft.Network/virtualNetworks'` , `'subnets'` ] ; const subnetInfo = new JsonResourceInfo ( [ jsonStringToTleExpression ( resName . unquotedValue ) , jsonStringToTleExpression ( subnetName . unquotedValue ) ] , subnetTypes , subnetObject , info ) ; results . push ( subnetInfo ) ; } } } } } } function isJsonStringAnExpression ( text ) { return text . length >= 2 && text [ 0 ] === '[' && text [ text . length - 1 ] === ']' ; } /**
 * Given a JSON string, turn it into a string representing a TLE subexpression by
 * either wrapping it with single quotes (if it's a plain JSON string value), or
 * removing the surrounding square brackets (if it's a TLE expression string)
 *
 * Examples:
 *   "mystring" => "'mystring'"
 *   "[variables('abc')]" => "variables('abc')"
 */ export function jsonStringToTleExpression ( stringUnquotedValue ) { if ( isJsonStringAnExpression ( stringUnquotedValue ) ) { return stringUnquotedValue . slice ( 1 , stringUnquotedValue . length - 1 ) ; } else { return `'${ stringUnquotedValue }'` ; } } export function splitResourceNameIntoSegments ( nameUnquotedValue ) { if ( isJsonStringAnExpression ( nameUnquotedValue ) ) { return splitExpressionIntoSegments ( nameUnquotedValue ) ; } return nameUnquotedValue . split ( '/' ) . map ( segment => `'${ segment }'` ) ; } function splitResourceTypeIntoSegments ( typeNameUnquotedValue ) { if ( ! typeNameUnquotedValue ) { return [ `''` ] ; } if ( ! isJsonStringAnExpression ( typeNameUnquotedValue ) ) { let segmentsAsStrings = typeNameUnquotedValue . split ( '/' ) ; if ( segmentsAsStrings . length >= 2 ) { segmentsAsStrings = [ `${ segmentsAsStrings [ 0 ] }/${ segmentsAsStrings [ 1 ] }` ] . concat ( segmentsAsStrings . slice ( 2 ) ) ; } return segmentsAsStrings . map ( segment => `'${ segment }'` ) ; } else { let segmentsAsExpressions = splitExpressionIntoSegments ( typeNameUnquotedValue ) ; if ( segmentsAsExpressions . length >= 2 ) { if ( isSingleQuoted ( segmentsAsExpressions [ 0 ] ) ) { if ( ! segmentsAsExpressions [ 0 ] . includes ( '/' ) ) { const combinedSegment = concatExpressionsWithSeparator ( segmentsAsExpressions . slice ( 0 , 2 ) , '/' ) ?? '' ; segmentsAsExpressions = [ combinedSegment ] . concat ( segmentsAsExpressions . slice ( 2 ) ) ; } } } return segmentsAsExpressions ; } } /**
 * Try to break up a expression (surrounded by square brackets) by handling certain common patterns
 */ function splitExpressionIntoSegments ( jsonString ) { assert ( jsonString [ 0 ] === '[' ) ; if ( jsonString . includes ( '/' ) ) { const quotedValue = `"${ jsonString }"` ; const parseResult = TLE . Parser . parse ( quotedValue ) ; const expression = parseResult . expression ; if ( parseResult . errors . length === 0 && expression ) { if ( expression instanceof TLE . FunctionCallValue && expression . isCallToBuiltinWithName ( 'concat' ) ) { const argumentExpressions = expression . argumentExpressions ; if ( argumentExpressions . every ( v => ! ! v ) ) { let rewrittenArgs = [ ] ; for ( let arg of argumentExpressions ) { if ( arg instanceof TLE . StringValue && arg . unquotedValue . includes ( '/' ) ) { const refactoredArg = splitStringAndKeepSeparators ( arg . unquotedValue , '/' ) . map ( s => `'${ s }'` ) ; rewrittenArgs . push ( ... refactoredArg ) ; } else { rewrittenArgs . push ( arg ! ) ; } } let segmentGroups = [ ] ; let newGroup = [ ] ; let separatorFound = false ; for ( let arg of rewrittenArgs ) { const argAsString = typeof arg === 'string' ? arg : arg . getSpan ( ) . getText ( quotedValue ) ; if ( arg === `'/'` ) { separatorFound = true ; segmentGroups . push ( newGroup ) ; newGroup = [ ] ; } else { newGroup . push ( argAsString ) ; } } segmentGroups . push ( newGroup ) ; if ( separatorFound && ! segmentGroups . some ( g => g . length === 0 ) ) { return segmentGroups . map ( g => g . length > 1 ? `concat(${ g . join ( ', ' ) })` : g [ 0 ] ) ; } } } } } return [ jsonStringToTleExpression ( jsonString ) ] ; } /**
 * Splits a string using a separator, but unlike string.split, the separators
 * are keep in the array returned.
 */ function splitStringAndKeepSeparators ( s , separator ) { let result = [ ] ; let substrings = s . split ( separator ) ; let first = true ; for ( let substring of substrings ) { if ( ! first ) { result . push ( '/' ) ; } first = false ; result . push ( substring ) ; } return result . filter ( s2 => s2 . length > 0 ) ; } function getFriendlyNameExpression ( { resource , fullName } ) { const resourceObject = ( "resourceObject" in resource ) ? resource . resourceObject : undefined ; let friendlyName ; let tags = resourceObject ?. getPropertyValue ( templateKeys . tags ) ?. asObjectValue ; let displayName = tags ?. getPropertyValue ( templateKeys . displayNameTag ) ?. asStringValue ?. unquotedValue ; if ( displayName ) { friendlyName = displayName ; } else { const name = fullName ? resource . getFullNameExpression ( ) : resource . shortNameExpression ; if ( name ) { friendlyName = getFriendlyExpressionFromTleExpression ( name ) ; } else { friendlyName = "(unnamed resource)" ; } } return friendlyName ; } function getFriendlyTypeExpression ( { resource , fullType } ) { let friendlyType = fullType ? resource . getFullTypeExpression ( ) : resource . shortTypeExpression ; friendlyType = friendlyType ? getFriendlyExpressionFromTleExpression ( friendlyType ) : '(no type)' ; return friendlyType ; } function getFriendlyResourceLabel ( { resource , fullName , fullType } ) { let nameLabel = getFriendlyNameExpression ( { resource , fullName } ) ; let typeLabel = getFriendlyTypeExpression ( { resource , fullType } ) ; return `${ nameLabel } (${ typeLabel })` ; } function findAndSetDecoupledChildren ( infos ) { const topLevel = infos . filter ( info => ! info . parent ) ; const possibleParents = topLevel ; const possibleChildren = topLevel . filter ( info => ! info . parent && info . nameSegmentExpressions . length > 1 ) ; for ( const parent of possibleParents ) { const removeIndices = [ ] ; possibleChildren . slice ( ) . forEach ( ( child , index ) => { assert ( ! child . parent , "Should have been removed from the array already" ) ; if ( areDecoupledChildAndParent ( child , parent ) ) { parent . children . push ( child ) ; child . parent = parent ; child . isDecoupledChild = true ; removeIndices . push ( index ) ; } } ) ; for ( let i = removeIndices . length - 1 ; i >= 0 ; -- i ) { possibleChildren . splice ( removeIndices [ i ] , 1 ) ; } if ( possibleChildren . length === 0 ) { break ; } } }