/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ import { getReferencesInExpression, TFExpressionSyntaxTree as tex, } from "@cdktn/hcl2json"; import { logger } from "./utils"; import { ProgramScope, Reference, TerraformResourceBlock } from "./types"; import { variableName } from "./variables"; export function referenceToVariableName( scope: ProgramScope, ref: Reference, ): string { const parts = ref.referencee.id.split("."); const resource = parts[0] === "data" ? `${parts[0]}.${parts[1]}` : parts[0]; const name = parts[0] === "data" ? parts[2] : parts[1]; return variableName(scope, resource, name); } export function containsReference(expression: tex.ExpressionType) { if (!tex.isScopeTraversalExpression(expression)) { return false; } const segments = expression.meta.traversal; const rootSegment = segments[0].segment; const fullAccessor = expression.meta.fullAccessor; if ( rootSegment === "count" || // count variable rootSegment === "each" || // each variable // https://www.terraform.io/docs/language/expressions/references.html#filesystem-and-workspace-info fullAccessor.startsWith("path.module") || fullAccessor.startsWith("path.root") || fullAccessor.startsWith("path.cwd") || fullAccessor.startsWith("terraform.workspace") || fullAccessor.startsWith("self.") // block local value ) { logger.debug(`skipping ${fullAccessor}`); return false; } return true; } export async function extractReferencesFromExpression( input: string, nodeIds: readonly string[], scopedIds: readonly string[] = [], // dynamics introduce new scoped variables that are not the globally accessible ids ): Promise { logger.debug(`extractReferencesFromExpression(${input})`); const possibleVariableSpots = await getReferencesInExpression( "main.tf", input, ); logger.debug( `found possible variable spots: ${JSON.stringify(possibleVariableSpots)}`, ); return possibleVariableSpots.reduce((carry, spot) => { const { value, startPosition, endPosition } = spot; // no reference if ( !value.includes(".") || // just a literal value.startsWith(".") || // dangling property access value.endsWith("...") || // spread (likely in for loop) value.startsWith("count.") || // count variable value.startsWith("each.") || // each variable // https://www.terraform.io/docs/language/expressions/references.html#filesystem-and-workspace-info value.startsWith("path.module") || value.startsWith("path.root") || value.startsWith("path.cwd") || value.startsWith("terraform.workspace") || value.startsWith("self.") // block local value ) { logger.debug(`skipping ${value}`); return carry; } const referenceParts = value.split("."); logger.debug( `Searching for node id '${value}' in ${JSON.stringify(nodeIds)}`, ); const corespondingNodeId = [...nodeIds, ...scopedIds].find((id) => { const parts = id.split("."); const matchesFirst = parts[0] === referenceParts[0]; const matchesFirstTwo = matchesFirst && (parts[1] === referenceParts[1] || referenceParts.length === 1); return ( matchesFirstTwo && (parts[0] === "data" ? parts[2] === referenceParts[2] : true) ); }); if (!corespondingNodeId) { // This is most likely a false positive, so we just ignore it // We include the log below to help debugging logger.error( `Found a reference that is unknown: ${input} has reference "${value}". The id was not found in ${JSON.stringify( nodeIds, )} with temporary values ${JSON.stringify(scopedIds)}.`, ); return carry; } if (scopedIds.includes(corespondingNodeId)) { logger.debug(`skipping '${value}' since it's a scoped variable`); return carry; } logger.debug(`Found node id '${corespondingNodeId}'`); const spotParts = value.split("."); let isThereANumericAccessor = false; const referenceSpotParts = spotParts.filter((part) => { if (!Number.isNaN(parseInt(part, 10))) { isThereANumericAccessor = true; return false; } return !isThereANumericAccessor; }); const fullReference = isThereANumericAccessor ? referenceSpotParts.slice(0, 2).join(".") : value; const isVariable = value.startsWith("var."); const useFqn = // Can not use FQN on vars !isVariable && // Can not use FQN on locals !value.startsWith("local.") && // If the following character is (input.substr(endPosition + 1, 1) === "*" || // a * (splat) we need to use the FQN input.substr(endPosition, 1) === "[" || // a property access isThereANumericAccessor); // a numeric access const ref: Reference = { start: startPosition, end: endPosition, referencee: { id: corespondingNodeId, full: fullReference, }, useFqn, isVariable, }; logger.debug(`Found reference ${JSON.stringify(ref)}`); return [...carry, ref]; }, [] as Reference[]); } export async function findUsedReferences( nodeIds: string[], item: TerraformResourceBlock, ): Promise { logger.debug(`findUsedReferences(${nodeIds}, ${item})`); if (typeof item === "string") { return await extractReferencesFromExpression(item, nodeIds, []); } if (typeof item !== "object" || item === null || item === undefined) { return []; } if (Array.isArray(item)) { return ( await Promise.all(item.map((i) => findUsedReferences(nodeIds, i))) ).flat(); } if (item && "dynamic" in item) { const dyn = (item as any)["dynamic"]; const { for_each: _for_each, ...others } = dyn; const dynamicRef = Object.keys(others)[0]; return await findUsedReferences([...nodeIds, dynamicRef], dyn); } return ( await Promise.all( Object.values(item as Record).map((i) => findUsedReferences(nodeIds, i), ), ) ).flat(); }