import * as assert from 'assert' ; import * as path from 'path' ; import { CodeAction , CodeActionContext , CodeActionKind , Command , DocumentLink , Range , Selection , Uri } from "vscode" ; import { IActionContext } from 'vscode-azureextensionui' ; import { configKeys , templateKeys } from "../../../common" ; import { TemplateScopeKind } from '../../../extension.bundle' ; import { ext } from '../../extensionVariables' ; import { AzureRMAssets , FunctionsMetadata } from "../../language/expressions/AzureRMAssets" ; import { isTleExpression } from '../../language/expressions/isTleExpression' ; import * as TLE from "../../language/expressions/TLE" ; import { INamedDefinition } from '../../language/INamedDefinition' ; import { Issue } from '../../language/Issue' ; import { IssueKind } from '../../language/IssueKind' ; import * as Json from "../../language/json/JSON" ; import { ReferenceList } from "../../language/ReferenceList" ; import { ContainsBehavior , Span } from "../../language/Span" ; import { CachedValue } from '../../util/CachedValue' ; import { CaseInsensitiveMap } from '../../util/CaseInsensitiveMap' ; import { expectParameterDocumentOrUndefined } from '../../util/expectDocument' ; import { filterByType } from '../../util/filterByType' ; import { filterNotUndefined } from '../../util/filterNotUndefined' ; import { Histogram } from '../../util/Histogram' ; import { nonNullValue } from '../../util/nonNull' ; import { FindReferencesAndErrorsVisitor } from "../../visitors/FindReferencesAndErrorsVisitor" ; import { FunctionCountVisitor } from "../../visitors/FunctionCountVisitor" ; import { GenericStringVisitor } from "../../visitors/GenericStringVisitor" ; import { ReferenceInVariableDefinitionsVisitor } from '../../visitors/ReferenceInVariableDefinitionsVisitor' ; import * as UndefinedVariablePropertyVisitor from "../../visitors/UndefinedVariablePropertyVisitor" ; import { getVSCodeRangeFromSpan } from '../../vscodeIntegration/vscodePosition' ; import { DeploymentDocument , ResolvableCodeLens } from "../DeploymentDocument" ; import { IParameterDefinitionsSource } from '../parameters/IParameterDefinitionsSource' ; import { IParameterValuesSourceProvider } from '../parameters/IParameterValuesSourceProvider' ; import { getMissingParameterErrors , getParameterValuesCodeActions } from '../parameters/ParameterValues' ; import { SynchronousParameterValuesSourceProvider } from "../parameters/SynchronousParameterValuesSourceProvider" ; import { TemplatePositionContext } from "../positionContexts/TemplatePositionContext" ; import { LinkedTemplateCodeLens , NestedTemplateCodeLens } from './ChildTemplateCodeLens' ; import { ParameterDefinitionCodeLens , SelectParameterFileCodeLens , ShowCurrentParameterFileCodeLens } from './deploymentTemplateCodeLenses' ; import { getResourcesInfo } from './getResourcesInfo' ; import { INotifyTemplateGraphArgs } from './linkedTemplates/linkedTemplates' ; import { getParentAndChildCodeLenses } from './ParentAndChildCodeLenses' ; import { isArmSchema } from './schemas' ; import { DeploymentScopeKind } from './scopes/DeploymentScopeKind' ; import { IDeploymentSchemaReference } from './scopes/IDeploymentSchemaReference' ; import { TemplateScope } from "./scopes/TemplateScope" ; import { IChildDeploymentScope , LinkedTemplateScope , NestedTemplateInnerScope , NestedTemplateOuterScope , TopLevelTemplateScope } from './scopes/templateScopes' ; import { UserFunctionParameterDefinition } from './UserFunctionParameterDefinition' ; export interface IScopedParseResult { parseResult : TLE . TleParseResult ; scope : TemplateScope ; } interface IAllReferences { referenceListsMap : Map < INamedDefinition | undefined , ReferenceList > ; issues : Issue [ ] ; } const resourceTypesNotAllowedInRGDeployments = [ "microsoft.blueprint/blueprints/artifacts" , "microsoft.blueprint/blueprints" , "microsoft.blueprint/blueprintAssignments" , "microsoft.blueprint/blueprints/versions" , "Microsoft.Authorization/policyDefinitions" , "Microsoft.Authorization/policySetDefinitions" , "Microsoft.Resources/resourceGroups" , "Microsoft.ManagedNetwork/scopeAssignments" , "Microsoft.Management/managementGroups" , "Microsoft.Subscription/aliases" , ] ; const resourceTypesNotAllowedInRGDeploymentsLC = resourceTypesNotAllowedInRGDeployments . map ( resType => resType . toLowerCase ( ) ) ; /**
 * Represents a deployment template file
 */ export class DeploymentTemplateDoc extends DeploymentDocument { private _topLevelScope : CachedValue < TemplateScope > = new CachedValue < TemplateScope > ( ) ; private _jsonStringValueToTleParseResultMap : CachedValue < Map < Json . StringValue , IScopedParseResult > > = new CachedValue < Map < Json . StringValue , IScopedParseResult > > ( ) ; private _allReferences : CachedValue < IAllReferences > = new CachedValue < IAllReferences > ( ) ; private _allScopes : CachedValue < TemplateScope [ ] > = new CachedValue < TemplateScope [ ] > ( ) ; public templateGraph : INotifyTemplateGraphArgs | undefined ; param _documentText param _documentUri constructor ( documentText , documentUri , public readonly documentVersion ) { super ( documentText , documentUri , documentVersion ) ; } public get topLevelScope ( ) : TemplateScope { return this . _topLevelScope . getOrCacheValue ( ( ) => new TopLevelTemplateScope ( this , this . topLevelValue , `Top-level template scope for ${ this . documentUri }` ) ) ; } public hasArmSchemaUri ( ) { return isArmSchema ( this . schemaUri ) ; } public get apiProfile ( ) : string | undefined { if ( this . topLevelValue ) { const apiProfileValue = Json . asStringValue ( this . topLevelValue . getPropertyValue ( templateKeys . apiProfile ) ) ; if ( apiProfileValue ) { return apiProfileValue . unquotedValue ; } } return undefined ; } /**
     * Parses all JSON strings in the template, assigns them a scope, and caches the results.
     * Returns a map that maps from the Json.StringValue object to the parse result (we can't cache
     * by the string value itself because those strings could have different scopes, and I don't
     * think the save in parsing of identical strings makes keying by scope and string value worth
     * the associated cost).
     */ private get quotedStringToTleParseResultMap ( ) : Map < Json . StringValue , IScopedParseResult > { return this . _jsonStringValueToTleParseResultMap . getOrCacheValue ( ( ) => { return StringParseAndScopeAssignmentVisitor . createParsedStringMap ( this ) ; } ) ; } public getErrorsCore ( _associatedParameters ) { const errors = [ ] ; errors . push ( ... this . allReferences . issues ) ; this . visitAllReachableStringValues ( jsonStringValue => { const jsonTokenStartIndex = jsonStringValue . span . startIndex ; const tleParseResult = this . getTLEParseResultFromJsonStringValue ( jsonStringValue ) ; const expressionScope = tleParseResult . scope ; const tleExpression = tleParseResult . parseResult . expression ; for ( const error of tleParseResult . parseResult . errors ) { errors . push ( error . translate ( jsonTokenStartIndex ) ) ; } const tleUndefinedVariablePropertyVisitor = UndefinedVariablePropertyVisitor . UndefinedVariablePropertyVisitor . visit ( tleExpression , expressionScope ) ; for ( const error of tleUndefinedVariablePropertyVisitor . errors ) { errors . push ( error . translate ( jsonTokenStartIndex ) ) ; } } ) ; const deploymentTemplateObject = Json . asObjectValue ( this . jsonParseResult . value ) ; if ( deploymentTemplateObject ) { const variablesObject = Json . asObjectValue ( deploymentTemplateObject . getPropertyValue ( templateKeys . variables ) ) ; if ( variablesObject ) { const referenceInVariablesFinder = new ReferenceInVariableDefinitionsVisitor ( this ) ; variablesObject . accept ( referenceInVariablesFinder ) ; for ( const referenceSpan of referenceInVariablesFinder . referenceSpans ) { errors . push ( new Issue ( referenceSpan , "reference() cannot be invoked inside of a variable definition." , IssueKind . referenceInVar ) ) ; } } } errors . push ( ... this . getMissingParameterErrors ( ) ) ; return errors ; } public getWarnings ( ) { const unusedWarnings = this . getUnusedDefinitionWarnings ( ) ; const inaccessibleScopeMembers = this . getInaccessibleScopeMemberWarnings ( ) ; const incorrectScopeWarnings = this . getIncorrectScopeWarnings ( ) ; const disabledValidationInfoWarnings = this . getDisabledValidationInfoWarnings ( ) ; return unusedWarnings . concat ( inaccessibleScopeMembers , incorrectScopeWarnings , disabledValidationInfoWarnings ) ; } private get allReferences ( ) : IAllReferences { return this . _allReferences . getOrCacheValue ( ( ) => { const referenceListsMap = new Map < INamedDefinition , ReferenceList > ( ) ; const issues = [ ] ; const functions = AzureRMAssets . getFunctionsMetadata ( ) ; this . visitAllReachableStringValues ( jsonStringValue => { const tleParseResult = this . getTLEParseResultFromJsonStringValue ( jsonStringValue ) ; if ( tleParseResult . parseResult . expression ) { const scope = tleParseResult . scope ; FindReferencesAndErrorsVisitor . visit ( scope , jsonStringValue . startIndex , tleParseResult . parseResult . expression , functions , referenceListsMap , issues ) ; } } ) ; return { referenceListsMap , issues } ; } ) ; } private getUnusedDefinitionWarnings ( ) { const warnings = [ ] ; const referenceListsMap = this . allReferences . referenceListsMap ; for ( const scope of this . uniqueNonExternalScopes ) { for ( const parameterDefinition of scope . parameterDefinitionsSource . parameterDefinitions ) { if ( ! referenceListsMap . has ( parameterDefinition ) ) { const message = parameterDefinition instanceof UserFunctionParameterDefinition ? `User-function parameter '${ parameterDefinition . nameValue . toString ( ) }' is never used.` : `The parameter '${ parameterDefinition . nameValue . toString ( ) }' is never used.` ; warnings . push ( new Issue ( parameterDefinition . nameValue . span , message , IssueKind . unusedParam ) ) ; } } for ( const variableDefinition of scope . variableDefinitions ) { if ( ! referenceListsMap . has ( variableDefinition ) ) { warnings . push ( new Issue ( variableDefinition . nameValue . span , `The variable '${ variableDefinition . nameValue . toString ( ) }' is never used.` , IssueKind . unusedVar ) ) ; } } for ( const ns of scope . namespaceDefinitions ) { for ( const member of ns . members ) { if ( ! referenceListsMap . has ( member ) ) { warnings . push ( new Issue ( member . nameValue . span , `The user-defined function '${ member . fullName }' is never used.` , IssueKind . unusedUdf ) ) ; } } } } return warnings ; } private getDisabledValidationInfoWarnings ( ) { const issues = [ ] ; if ( this . templateGraph && ! this . templateGraph . fullValidationStatus . fullValidationEnabled ) { for ( const scope of this . allScopes ) { if ( scope instanceof NestedTemplateOuterScope || scope instanceof NestedTemplateInnerScope || scope instanceof LinkedTemplateScope ) { let span = scope . owningDeploymentResource . nameValue ?. span ?? scope . owningDeploymentResource . span ; const kind = scope instanceof LinkedTemplateScope ? IssueKind . cannotValidateLinkedTemplate : IssueKind . cannotValidateNestedTemplate ; const message = `${ kind === IssueKind . cannotValidateLinkedTemplate ? 'Linked template' : 'Nested template' } "${ scope . owningDeploymentResource . nameValue ?. unquotedValue ?? 'unknown' }" will not have validation or parameter completion. To enable, either add default values to all top-level parameters or add a parameter file ("Select/Create Parameter File" command).` ; issues . push ( new Issue ( span , message , kind ) ) ; } } } return issues ; } /**
     * Finds parameters/variables/functions inside outer-scoped nested templates, which
     * by definition can't be accessed in any expressions.
     */ private getInaccessibleScopeMemberWarnings ( ) { const warnings = [ ] ; const warningMessage = 'Variables, parameters and user functions of an outer-scoped nested template are inaccessible to any expressions. ' + `If you intended inner scope, set the deployment resource's properties.expressionEvaluationOptions.scope to 'inner'.` ; for ( const scope of this . allScopes ) { if ( scope instanceof NestedTemplateOuterScope ) { const parameters = getPropertyValueOfScope ( templateKeys . parameters ) ; if ( ! ! parameters ?. asObjectValue ?. properties ?. length ) { warnings . push ( new Issue ( parameters . span , warningMessage , IssueKind . inaccessibleNestedScopeMembers ) ) ; } const variables = getPropertyValueOfScope ( templateKeys . variables ) ; if ( ! ! variables ?. asObjectValue ?. properties . length ) { warnings . push ( new Issue ( variables . span , warningMessage , IssueKind . inaccessibleNestedScopeMembers ) ) ; } const namespaces = getPropertyValueOfScope ( templateKeys . functions ) ; if ( ! ! namespaces ?. asArrayValue ?. elements . length ) { warnings . push ( new Issue ( namespaces . span , warningMessage , IssueKind . inaccessibleNestedScopeMembers ) ) ; } const propertyValues = scope . parameterValuesProperty ; if ( ! ! propertyValues ?. value ?. asObjectValue ?. properties . length ) { warnings . push ( new Issue ( propertyValues . span , warningMessage , IssueKind . inaccessibleNestedScopeMembers ) ) ; } function getPropertyValueOfScope ( propertyName ) { return scope . rootObject ?. getProperty ( propertyName ) ?. value ; } } } return warnings ; } /**
     * Finds parameters/variables/functions inside outer-scoped nested templates, which
     * by definition can't be accessed in any expressions.
     */ private getIncorrectScopeWarnings ( ) { const warnings = [ ] ; const scope = this . topLevelScope ; const deploymentSchema = scope . deploymentSchema ; if ( deploymentSchema ?. matchingInfo ?. deploymentScopeKind === DeploymentScopeKind . resourceGroup ) { for ( const resource of scope . resources ) { if ( resource . resourceTypeValue ) { const resourceTypeLC = resource . resourceTypeValue . asStringValue ?. unquotedValue . toLowerCase ( ) ; if ( resourceTypeLC && resourceTypesNotAllowedInRGDeploymentsLC . includes ( resourceTypeLC ) ) { const warningMessage = `This resource type may not available for a deployment scoped to resource group. Are you using the correct schema?` ; const warning = new Issue ( resource . resourceTypeValue . span , warningMessage , IssueKind . incorrectScopeWarning ) ; const schemaSpan = deploymentSchema . schemaStringValue ?. span ; if ( schemaSpan ) { warning . relatedInformation . push ( { location : { uri : this . documentUri , span : schemaSpan } , message : "The schema is specified here" } ) ; } warnings . push ( warning ) ; } } } } return warnings ; } private getMissingParameterErrors ( ) { const errors = [ ] ; for ( const scope of this . uniqueScopes ) { if ( scope . parameterValuesSource ) { const scopeErrors = getMissingParameterErrors ( scope . parameterValuesSource , scope . parameterDefinitionsSource ) ; errors . push ( ... scopeErrors ) ; } } return errors ; } /**
     * Gets info about TLE function usage, useful for telemetry
     */ public getFunctionCounts ( ) { const functionCounts = new Histogram ( ) ; if ( this . jsonParseResult . value ) { GenericStringVisitor . visit ( this . jsonParseResult . value , ( stringValue ) : void => { const tleParseResult = this . getTLEParseResultFromJsonStringValue ( stringValue ) ; let tleFunctionCountVisitor = FunctionCountVisitor . visit ( tleParseResult . parseResult . expression ) ; functionCounts . add ( tleFunctionCountVisitor . functionCounts ) ; } ) ; } return functionCounts ; } /**
     * Gets info about schema usage, useful for telemetry
     */ public getResourceUsage ( availableResourceTypesAndVersions ) { const resourceCounts = new Histogram ( ) ; const invalidResourceCounts = new Histogram ( ) ; const invalidVersionCounts = new Histogram ( ) ; const apiProfileString = `(profile=${ this . apiProfile || 'none' })` . toLowerCase ( ) ; const resources = this . topLevelValue ? Json . asArrayValue ( this . topLevelValue . getPropertyValue ( templateKeys . resources ) ) : undefined ; if ( resources ) { traverseResources ( resources , undefined ) ; if ( availableResourceTypesAndVersions . size > 0 ) { for ( const key of resourceCounts . keys ) { if ( key ) { const count = resourceCounts . getCount ( key ) ; let apiVersionsForType ; let apiVersion ; let resourceType ; const match = key . match ( /([a-z./0-9-]+)@([a-z0-9-]+)/ ) ; if ( match ) { resourceType = match [ 1 ] ; apiVersion = match [ 2 ] ; apiVersionsForType = availableResourceTypesAndVersions . get ( resourceType ) ; } else { resourceType = key ; apiVersion = "" ; } if ( apiVersionsForType ) { if ( ! apiVersionsForType . includes ( apiVersion ) ) { invalidVersionCounts . add ( key , count ) ; } } else { invalidResourceCounts . add ( key , count ) ; } } } } } return [ resourceCounts , invalidResourceCounts , invalidVersionCounts ] ; function traverseResources ( resourcesObject , parentKey ) { for ( let resource of resourcesObject . elements ) { const resourceObject = Json . asObjectValue ( resource ) ; if ( resourceObject ) { const resourceType = Json . asStringValue ( resourceObject . getPropertyValue ( templateKeys . resourceType ) ) ; if ( resourceType ) { const apiVersion = Json . asStringValue ( resourceObject . getPropertyValue ( templateKeys . resourceApiVersion ) ) ; let apiVersionString = apiVersion ? apiVersion . unquotedValue . trim ( ) . toLowerCase ( ) : undefined ; if ( ! apiVersionString ) { apiVersionString = apiProfileString ; } else { if ( apiVersionString . startsWith ( '[' ) ) { apiVersionString = '[expression]' ; } } let resourceTypeString = resourceType . unquotedValue . trim ( ) . toLowerCase ( ) ; if ( resourceTypeString . startsWith ( '[' ) ) { resourceTypeString = "[expression]" ; } let simpleKey = `${ resourceTypeString }@${ apiVersionString }` ; const fullKey = parentKey ? `${ simpleKey }[parent=${ parentKey }]` : simpleKey ; resourceCounts . add ( fullKey ) ; const childResources = Json . asArrayValue ( resourceObject . getPropertyValue ( templateKeys . resources ) ) ; if ( childResources ) { traverseResources ( childResources , simpleKey ) ; } } } } } } public getMultilineStringCount ( ) { let count = 0 ; this . visitAllReachableStringValues ( jsonStringValue => { if ( jsonStringValue . unquotedValue . indexOf ( "\n" ) >= 0 ) { ++ count ; } } ) ; return count ; } public getChildTemplatesInfo ( ) { const scopes = this . allScopes ; const linkedTemplateScopes = filterByType ( scopes , LinkedTemplateScope ) ; const templateLinkObjects = filterNotUndefined ( linkedTemplateScopes . map ( lts => lts . templateLinkObject ) ) ; return { nestedOuterCount : scopes . filter ( s => s . scopeKind === TemplateScopeKind . NestedDeploymentWithOuterScope ) . length , nestedInnerCount : scopes . filter ( s => s . scopeKind === TemplateScopeKind . NestedDeploymentWithInnerScope ) . length , linkedTemplatesCount : linkedTemplateScopes . length , linkedTemplatesUriCount : templateLinkObjects . filter ( tl => tl . getPropertyValue ( templateKeys . linkedDeploymentTemplateLinkUri ) ?. asStringValue ) . length , linkedTemplatesRelativePathCount : templateLinkObjects . filter ( tl => tl . getPropertyValue ( templateKeys . linkedDeploymentTemplateLinkRelativePath ) ?. asStringValue ) . length } ; } public getContextFromDocumentLineAndColumnIndexes ( documentLineIndex , documentColumnIndex , associatedParameters , allowOutOfBounds = true ) { return TemplatePositionContext . fromDocumentLineAndColumnIndexes ( this , documentLineIndex , documentColumnIndex , expectParameterDocumentOrUndefined ( associatedParameters ) , allowOutOfBounds ) ; } public getContextFromDocumentCharacterIndex ( documentCharacterIndex , associatedParameters , allowOutOfBounds = true ) { return TemplatePositionContext . fromDocumentCharacterIndex ( this , documentCharacterIndex , expectParameterDocumentOrUndefined ( associatedParameters ) , allowOutOfBounds ) ; } /**
     * Get the TLE parse results from this JSON string.
     */ public getTLEParseResultFromJsonStringValue ( jsonStringValue ) { const result = this . quotedStringToTleParseResultMap . get ( jsonStringValue ) ; if ( result ) { return result ; } const tleParseResult = TLE . Parser . parse ( jsonStringValue . quotedValue ) ; const scopedResult = { parseResult : tleParseResult , scope : this . topLevelScope } ; this . quotedStringToTleParseResultMap . set ( jsonStringValue , scopedResult ) ; return scopedResult ; } public findReferencesToDefinition ( definition ) { const result = new ReferenceList ( definition . definitionKind ) ; if ( definition . nameValue ) { result . add ( { document : this , span : definition . nameValue . unquotedSpan } ) ; } const referencesList = this . allReferences . referenceListsMap . get ( definition ) ; if ( referencesList ) { result . addAll ( referencesList ) ; } return result ; } private visitAllReachableStringValues ( onStringValue ) { let value = this . topLevelValue ; if ( value ) { GenericStringVisitor . visit ( value , onStringValue ) ; } } public getCodeActions ( _associatedDocument , range , context ) { const actions = [ ] ; for ( const scope of this . uniqueScopes ) { if ( scope . parameterValuesSource ) { let parentParameterDefinitionsSource = scope . parentWithUniqueParamsVarsAndFunctions . parameterDefinitionsSource ; const scopeActions = getParameterValuesCodeActions ( scope . parameterValuesSource , scope . parameterDefinitionsSource , parentParameterDefinitionsSource , range , context ) ; actions . push ( ... scopeActions ) ; } } actions . push ( ... this . getExtractVarParamCodeActions ( range ) ) ; return actions ; } private getExtractVarParamCodeActions ( range ) { let shouldAddExtractActions = false ; let pc = this . getContextFromDocumentLineAndColumnIndexes ( range . start . line , range . start . character , undefined ) ; if ( range . start . line === range . end . line ) { if ( pc . jsonToken && pc . jsonTokenStartIndex > 0 ) { let jsonToken = pc . document . getJSONValueAtDocumentCharacterIndex ( pc . jsonTokenStartIndex - 1 , ContainsBehavior . extended ) ; if ( ( jsonToken instanceof Json . Property || jsonToken instanceof Json . ArrayValue ) && pc . document . topLevelValue ) { let scope = pc . getScope ( ) ; if ( ! scope . rootObject ) { return [ ] ; } let resources = scope . rootObject . getPropertyValue ( templateKeys . resources ) ; if ( ! resources || ! resources . span . intersect ( jsonToken . span ) ) { return [ ] ; } let jsonValue = pc . document . getJSONValueAtDocumentCharacterIndex ( pc . jsonTokenStartIndex , ContainsBehavior . extended ) ; if ( ! jsonValue ) { return [ ] ; } const stringValue = jsonValue . asStringValue ; if ( stringValue ) { if ( ! range . isEmpty ) { let startIndex = this . getDocumentCharacterIndex ( range . start . line , range . start . character ) ; let endIndex = this . getDocumentCharacterIndex ( range . end . line , range . end . character ) ; let span = new Span ( startIndex , endIndex - startIndex ) ; const selectedText = this . getDocumentTextWithSquareBrackets ( span ) ; if ( this . isParameterOrVariableReference ( selectedText ) ) { return [ ] ; } if ( pc . jsonValue && jsonValue . span === pc . jsonValue . span && selectedText && this . equalsWithSqareBrackets ( pc . jsonValue . asStringValue ?. unquotedValue , selectedText ) ) { shouldAddExtractActions = true ; } else { if ( isTleExpression ( stringValue . unquotedValue ) ) { shouldAddExtractActions = this . isValidExpression ( this . getDocumentTextWithSurroundingCharacters ( span , "'" , "'" ) ) ; } } } else { if ( this . isSimpleText ( stringValue . quotedValue ) && pc . jsonValue && jsonValue . span === pc . jsonValue . span ) { shouldAddExtractActions = true ; } } } } } } if ( shouldAddExtractActions ) { return [ this . createExtractCommand ( 'Extract Parameter...' , 'extractParameter' ) , this . createExtractCommand ( 'Extract Variable...' , 'extractVariable' ) , ] ; } return [ ] ; } private getDocumentTextWithSquareBrackets ( span ) { let text = this . getDocumentText ( span ) ; if ( text . startsWith ( "[" ) && text . endsWith ( "]" ) ) { return text ; } let extendedSpan = span . extendLeft ( 1 ) . extendRight ( 1 ) ; let extendedText = this . getDocumentText ( extendedSpan ) ; if ( extendedText . startsWith ( "[" ) && extendedText . endsWith ( "]" ) ) { return extendedText ; } return text ; } private getDocumentTextWithSurroundingCharacters ( span , start , end ) { let text = this . getDocumentText ( span ) ; if ( text . startsWith ( start ) && text . endsWith ( end ) ) { return text ; } let extendedSpan = span . extendLeft ( 1 ) . extendRight ( 1 ) ; let extendedText = this . getDocumentText ( extendedSpan ) ; if ( extendedText . startsWith ( start ) && extendedText . endsWith ( end ) ) { return extendedText ; } return text ; } private isParameterOrVariableReference ( text ) { const regEx = /^"?\[?(parameters|variables)\('.+'\)]?"?/gi ; return regEx . test ( text ) ; } private isSimpleText ( text ) { if ( text . startsWith ( "\"[" ) || text . endsWith ( "]\"" ) ) { return false ; } return text . startsWith ( "\"" ) && text . endsWith ( "\"" ) ; } public isValidExpression ( text ) { const functionCallRegex = /^\s*[\w\.]+\(.*\)\s*$/gi ; const textRegex = /^\s*'.+'\s*$/gi ; return functionCallRegex . test ( text ) || textRegex . test ( text ) ; } private equalsWithSqareBrackets ( text , selectedText ) { if ( ! text ) { return false ; } if ( text === selectedText ) { return true ; } if ( text . startsWith ( "[" ) && text . endsWith ( "]" ) ) { text = text . substr ( 1 , text . length - 2 ) ; } return text === selectedText ; } private createExtractCommand ( title , command ) { const action = new CodeAction ( title , CodeActionKind . RefactorExtract ) ; action . command = { command : `azurerm-vscode-tools.codeAction.${ command }` , title : '' } ; return action ; } public getTextAtTleValue ( tleValue , parentStringToken ) { assert . equal ( parentStringToken . type , Json . TokenType . QuotedString ) ; const spanOfValueInsideString = tleValue . getSpan ( ) ; return this . getDocumentText ( spanOfValueInsideString , parentStringToken . span . startIndex ) ; } param topLevelParameterValuesProvider public getCodeLenses ( topLevelParameterValuesProvider ) { const lenses = [ ] ; for ( const scope of this . allScopes ) { if ( scope . hasUniqueParamsVarsAndFunctions ) { let paramValuesSourceProvider ; if ( scope instanceof TopLevelTemplateScope ) { paramValuesSourceProvider = topLevelParameterValuesProvider ; } else { const parameterValuesSource = scope . parameterValuesSource ; paramValuesSourceProvider = parameterValuesSource ? new SynchronousParameterValuesSourceProvider ( parameterValuesSource ) : undefined ; } const codelenses = this . getParameterCodeLenses ( scope , paramValuesSourceProvider ) ; lenses . push ( ... codelenses ) ; } const infos = getResourcesInfo ( { scope , recognizeDecoupledChildren : true } ) ; lenses . push ( ... getParentAndChildCodeLenses ( scope , infos ) ) ; } lenses . push ( ... this . getChildTemplateCodeLenses ( topLevelParameterValuesProvider ) ) ; return lenses ; } private getParameterCodeLenses ( uniqueScope , parameterValuesSourceProvider ) { if ( ! ext . configuration . get < boolean > ( configKeys . codeLensForParameters ) ) { return [ ] ; } const lenses = [ ] ; if ( uniqueScope . isExternal ) { return [ ] ; } if ( uniqueScope instanceof TopLevelTemplateScope ) { const parametersCodeLensSpan = uniqueScope . rootObject ?. getProperty ( templateKeys . parameters ) ?. span ?? new Span ( 0 , 0 ) ; const parameterFileUri = parameterValuesSourceProvider ?. parameterFileUri ; if ( parameterFileUri ) { assert ( uniqueScope instanceof TopLevelTemplateScope , "Expecting top-level scope" ) ; lenses . push ( new ShowCurrentParameterFileCodeLens ( uniqueScope , parametersCodeLensSpan , parameterFileUri ) ) ; } lenses . push ( new SelectParameterFileCodeLens ( this . topLevelScope , parametersCodeLensSpan , parameterFileUri , { fullValidationStatus : this . templateGraph ?. fullValidationStatus } ) ) ; } if ( parameterValuesSourceProvider ) { lenses . push ( ... uniqueScope . parameterDefinitionsSource . parameterDefinitions . map ( pd => new ParameterDefinitionCodeLens ( uniqueScope , pd , parameterValuesSourceProvider ) ) ) ; } return lenses ; } private getChildTemplateCodeLenses ( topLevelParameterValuesProvider ) { const lenses = [ ] ; for ( let scope of this . allScopes ) { const owningDeploymentResource = ( < Partial < IChildDeploymentScope > > scope ) . owningDeploymentResource ; if ( scope . rootObject || owningDeploymentResource ) { switch ( scope . scopeKind ) { case TemplateScopeKind . NestedDeploymentWithInnerScope : case TemplateScopeKind . NestedDeploymentWithOuterScope : if ( scope . rootObject ) { lenses . push ( ... NestedTemplateCodeLens . create ( this . templateGraph ?. fullValidationStatus , scope , scope . rootObject . span , topLevelParameterValuesProvider ) ) ; } break ; case TemplateScopeKind . LinkedDeployment : assert ( scope instanceof LinkedTemplateScope , "Expected a LinkedTemplateScope" ) ; if ( owningDeploymentResource ) { const templateLinkObject = scope . templateLinkObject ; let span = templateLinkObject ? templateLinkObject . span : owningDeploymentResource . span ; lenses . push ( ... LinkedTemplateCodeLens . create ( this . templateGraph ?. fullValidationStatus , scope , span , scope . linkedFileReferences , topLevelParameterValuesProvider ) ) ; } break ; default : break ; } } } return lenses ; } public getDocumentLinks ( context ) { const links = [ ] ; for ( const scope of filterByType ( this . allScopes , LinkedTemplateScope ) ) { const templateLinkObject = scope . templateLinkObject ; const relativePathStringValue = templateLinkObject ?. getPropertyValue ( templateKeys . linkedDeploymentTemplateLinkRelativePath ) ?. asStringValue ; const relativePathValue = relativePathStringValue ?. unquotedValue ; if ( relativePathStringValue && relativePathValue && ! isTleExpression ( relativePathValue ) ) { const link = new DocumentLink ( getVSCodeRangeFromSpan ( this , relativePathStringValue . unquotedSpan ) ) ; link . target = Uri . file ( path . resolve ( path . dirname ( this . documentUri . fsPath ) , relativePathValue ) ) ; links . push ( link ) ; } } return links ; } /**
     * Returns all scopes which actually host unique members
     */ public get uniqueScopes ( ) : TemplateScope [ ] { return this . allScopes . filter ( scope => scope . hasUniqueParamsVarsAndFunctions ) ; } /**
     * Returns all scopes which actually host unique members and whose members are not defined
     * external (linked templates)
     */ public get uniqueNonExternalScopes ( ) : TemplateScope [ ] { return this . allScopes . filter ( scope => scope . hasUniqueParamsVarsAndFunctions && ! scope . isExternal ) ; } /**
     * Returns all scopes, including those that just repeat their parents' scopes
     */ public get allScopes ( ) : TemplateScope [ ] { return this . _allScopes . getOrCacheValue ( ( ) => { let scopes = [ this . topLevelScope ] ; traverse ( this . topLevelScope ) ; return scopes ; function traverse ( scope ) { for ( let childScope of scope ?. childScopes ?? [ ] ) { assert ( scopes . indexOf ( childScope ) < 0 , "Already in array" ) ; scopes . push ( childScope ) ; traverse ( childScope ) ; } } } ) ; } } class StringParseAndScopeAssignmentVisitor extends Json . Visitor { private readonly _jsonStringValueToTleParseResultMap : Map < Json . StringValue , IScopedParseResult > = new Map < Json . StringValue , IScopedParseResult > ( ) ; private readonly _scopeStack : TemplateScope [ ] = [ ] ; private _currentScope : TemplateScope ; private readonly _uniqueTemplateScopes : TemplateScope [ ] = [ ] ; public constructor ( private readonly _dt ) { super ( ) ; this . _currentScope = _dt . topLevelScope ; this . _uniqueTemplateScopes = _dt . uniqueScopes ; } public static createParsedStringMap ( dt ) { const visitor = new StringParseAndScopeAssignmentVisitor ( dt ) ; return visitor . createMap ( ) ; } private createMap ( ) { this . _dt . topLevelValue ?. accept ( this ) ; return this . _jsonStringValueToTleParseResultMap ; } public visitStringValue ( jsonStringValue ) { assert ( ! this . _jsonStringValueToTleParseResultMap . has ( jsonStringValue ) , "Already parsed this string" ) ; let tleParseResult = TLE . Parser . parse ( jsonStringValue . quotedValue ) ; this . _jsonStringValueToTleParseResultMap . set ( jsonStringValue , { parseResult : tleParseResult , scope : this . _currentScope } ) ; } public visitObjectValue ( jsonObjectValue ) { const currentScope = this . _currentScope ; const newScope = this . _uniqueTemplateScopes . find ( scope => scope . rootObject === jsonObjectValue ) ; if ( newScope ) { this . _scopeStack . push ( this . _currentScope ) ; this . _currentScope = newScope ; } super . visitObjectValue ( jsonObjectValue ) ; if ( newScope ) { assert ( this . _currentScope === newScope ) ; this . _currentScope = nonNullValue ( this . _scopeStack . pop ( ) , "Scopes stack should not be empty" ) ; assert ( this . _currentScope === currentScope ) ; } } }