diff --git a/.vscode-test.mjs b/.vscode-test.mjs index b62ba25..f728f01 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,5 +1,5 @@ -import { defineConfig } from '@vscode/test-cli'; +import { defineConfig } from "@vscode/test-cli"; export default defineConfig({ - files: 'out/test/**/*.test.js', + files: "out/test/**/*.test.js", }); diff --git a/eslint.config.mjs b/eslint.config.mjs index d5c0b53..ccf7156 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,28 +1,34 @@ import typescriptEslint from "@typescript-eslint/eslint-plugin"; import tsParser from "@typescript-eslint/parser"; -export default [{ +export default [ + { files: ["**/*.ts"], -}, { + }, + { plugins: { - "@typescript-eslint": typescriptEslint, + "@typescript-eslint": typescriptEslint, }, languageOptions: { - parser: tsParser, - ecmaVersion: 2022, - sourceType: "module", + parser: tsParser, + ecmaVersion: 2022, + sourceType: "module", }, rules: { - "@typescript-eslint/naming-convention": ["warn", { - selector: "import", - format: ["camelCase", "PascalCase"], - }], + "@typescript-eslint/naming-convention": [ + "warn", + { + selector: "import", + format: ["camelCase", "PascalCase"], + }, + ], - curly: "warn", - eqeqeq: "warn", - "no-throw-literal": "warn", - semi: "warn", + curly: "warn", + eqeqeq: "warn", + "no-throw-literal": "warn", + semi: "warn", }, -}]; \ No newline at end of file + }, +]; diff --git a/src/extension.ts b/src/extension.ts index 4b21a72..748bc56 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,8 +1,9 @@ -import * as vscode from 'vscode'; -import { TerraformInlayProvider } from './providers/inlayProvider'; -import { TerraformVariableResolver } from './resolvers/variableResolver'; -import { Logger } from './utils/logger'; -import { ConfigurationManager } from './utils/configurationManager'; +import * as vscode from "vscode"; + +import { TerraformInlayProvider } from "./providers/inlayProvider"; +import { TerraformVariableResolver } from "./resolvers/variableResolver"; +import { ConfigurationManager } from "./utils/configurationManager"; +import { Logger } from "./utils/logger"; let logger: Logger; let configManager: ConfigurationManager; @@ -10,231 +11,283 @@ let inlayProviders: Map = new Map(); let statusBarItem: vscode.StatusBarItem; export function activate(context: vscode.ExtensionContext) { - // Initialize logger - logger = new Logger('TerraformVariableResolver'); - configManager = new ConfigurationManager(); - - // Create status bar item - statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); - statusBarItem.command = 'terraform-resolver.toggle'; - updateStatusBar(); - statusBarItem.show(); - - logger.info('Terraform Variable Resolver activating...'); - - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { - logger.warn('No workspace folders found, extension will not activate'); - vscode.window.showWarningMessage('Terraform Variable Resolver: No workspace folder found'); - return; - } + // Initialize logger + logger = new Logger("TerraformVariableResolver"); + configManager = new ConfigurationManager(); + + // Create status bar item + statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100 + ); + statusBarItem.command = "terraform-resolver.toggle"; + updateStatusBar(); + statusBarItem.show(); + + logger.info("Terraform Variable Resolver activating..."); + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + logger.warn("No workspace folders found, extension will not activate"); + vscode.window.showWarningMessage( + "Terraform Variable Resolver: No workspace folder found" + ); + return; + } - // Initialize providers for each workspace folder - for (const folder of workspaceFolders) { - try { - const workspaceRoot = folder.uri.fsPath; - const provider = new TerraformInlayProvider(workspaceRoot, logger, configManager); - inlayProviders.set(workspaceRoot, provider); - - // Register inlay hints provider - const inlayProviderDisposable = vscode.languages.registerInlayHintsProvider( - { scheme: 'file', language: 'terraform', pattern: `${workspaceRoot}/**/*.tf` }, - provider - ); - - context.subscriptions.push(inlayProviderDisposable); - logger.info(`Inlay provider registered for workspace: ${folder.name}`); - } catch (error) { - logger.error(`Failed to initialize provider for workspace ${folder.name}`, error); - vscode.window.showErrorMessage(`Terraform Variable Resolver: Failed to initialize for workspace ${folder.name}`); - } + // Initialize providers for each workspace folder + for (const folder of workspaceFolders) { + try { + const workspaceRoot = folder.uri.fsPath; + const provider = new TerraformInlayProvider( + workspaceRoot, + logger, + configManager + ); + inlayProviders.set(workspaceRoot, provider); + + // Register inlay hints provider + const inlayProviderDisposable = + vscode.languages.registerInlayHintsProvider( + { + scheme: "file", + language: "terraform", + pattern: `${workspaceRoot}/**/*.tf`, + }, + provider + ); + + context.subscriptions.push(inlayProviderDisposable); + logger.info(`Inlay provider registered for workspace: ${folder.name}`); + } catch (error) { + logger.error( + `Failed to initialize provider for workspace ${folder.name}`, + error + ); + vscode.window.showErrorMessage( + `Terraform Variable Resolver: Failed to initialize for workspace ${folder.name}` + ); } + } + + // Register commands + registerCommands(context); + + // Register configuration change handler + const configChangeDisposable = vscode.workspace.onDidChangeConfiguration( + (event) => { + if (event.affectsConfiguration("terraformResolver")) { + configManager.reload(); + refreshAllProviders(); + updateStatusBar(); + logger.info("Configuration changed, providers refreshed"); + } + } + ); - // Register commands - registerCommands(context); - - // Register configuration change handler - const configChangeDisposable = vscode.workspace.onDidChangeConfiguration(event => { - if (event.affectsConfiguration('terraformResolver')) { - configManager.reload(); - refreshAllProviders(); - updateStatusBar(); - logger.info('Configuration changed, providers refreshed'); - } - }); - - // Register workspace folder changes - const workspaceFolderChangeDisposable = vscode.workspace.onDidChangeWorkspaceFolders(event => { - handleWorkspaceFolderChanges(event, context); + // Register workspace folder changes + const workspaceFolderChangeDisposable = + vscode.workspace.onDidChangeWorkspaceFolders((event) => { + handleWorkspaceFolderChanges(event, context); }); - context.subscriptions.push( - configChangeDisposable, - workspaceFolderChangeDisposable, - statusBarItem - ); + context.subscriptions.push( + configChangeDisposable, + workspaceFolderChangeDisposable, + statusBarItem + ); - logger.info('Terraform Variable Resolver activated successfully'); - vscode.window.showInformationMessage('Terraform Variable Resolver activated!'); + logger.info("Terraform Variable Resolver activated successfully"); + vscode.window.showInformationMessage( + "Terraform Variable Resolver activated!" + ); } function registerCommands(context: vscode.ExtensionContext) { - const commands = [ - vscode.commands.registerCommand('terraform-resolver.refresh', async () => { - try { - await refreshAllProviders(); - vscode.window.showInformationMessage('Terraform variable cache refreshed successfully'); - logger.info('Manual refresh completed'); - } catch (error) { - logger.error('Failed to refresh cache', error); - vscode.window.showErrorMessage('Failed to refresh Terraform cache'); - } - }), - - vscode.commands.registerCommand('terraform-resolver.toggle', async () => { - try { - const enabled = configManager.isEnabled(); - await configManager.setEnabled(!enabled); - updateStatusBar(); - - const message = `Terraform inlay hints ${!enabled ? 'enabled' : 'disabled'}`; - vscode.window.showInformationMessage(message); - logger.info(message); - } catch (error) { - logger.error('Failed to toggle inlay hints', error); - vscode.window.showErrorMessage('Failed to toggle Terraform inlay hints'); - } - }), - - vscode.commands.registerCommand('terraform-resolver.clearCache', async () => { - try { - for (const provider of inlayProviders.values()) { - provider.clearCache(); - } - vscode.window.showInformationMessage('Terraform cache cleared successfully'); - logger.info('Cache cleared manually'); - } catch (error) { - logger.error('Failed to clear cache', error); - vscode.window.showErrorMessage('Failed to clear Terraform cache'); - } - }), - - vscode.commands.registerCommand('terraform-resolver.showLogs', () => { - logger.show(); - }), - - vscode.commands.registerCommand('terraform-resolver.diagnostics', async () => { - try { - await showDiagnostics(); - } catch (error) { - logger.error('Failed to show diagnostics', error); - vscode.window.showErrorMessage('Failed to show diagnostics'); - } - }) - ]; - - context.subscriptions.push(...commands); -} + const commands = [ + vscode.commands.registerCommand("terraform-resolver.refresh", async () => { + try { + await refreshAllProviders(); + vscode.window.showInformationMessage( + "Terraform variable cache refreshed successfully" + ); + logger.info("Manual refresh completed"); + } catch (error) { + logger.error("Failed to refresh cache", error); + vscode.window.showErrorMessage("Failed to refresh Terraform cache"); + } + }), + + vscode.commands.registerCommand("terraform-resolver.toggle", async () => { + try { + const enabled = configManager.isEnabled(); + await configManager.setEnabled(!enabled); + updateStatusBar(); + + const message = `Terraform inlay hints ${!enabled ? "enabled" : "disabled"}`; + vscode.window.showInformationMessage(message); + logger.info(message); + } catch (error) { + logger.error("Failed to toggle inlay hints", error); + vscode.window.showErrorMessage( + "Failed to toggle Terraform inlay hints" + ); + } + }), + + vscode.commands.registerCommand( + "terraform-resolver.clearCache", + async () => { + try { + for (const provider of inlayProviders.values()) { + provider.clearCache(); + } + vscode.window.showInformationMessage( + "Terraform cache cleared successfully" + ); + logger.info("Cache cleared manually"); + } catch (error) { + logger.error("Failed to clear cache", error); + vscode.window.showErrorMessage("Failed to clear Terraform cache"); + } + } + ), -async function refreshAllProviders(): Promise { - const promises = Array.from(inlayProviders.values()).map(async provider => { + vscode.commands.registerCommand("terraform-resolver.showLogs", () => { + logger.show(); + }), + + vscode.commands.registerCommand( + "terraform-resolver.diagnostics", + async () => { try { - await provider.refresh(); + await showDiagnostics(); } catch (error) { - logger.error('Failed to refresh provider', error); - throw error; + logger.error("Failed to show diagnostics", error); + vscode.window.showErrorMessage("Failed to show diagnostics"); } - }); + } + ), + ]; + + context.subscriptions.push(...commands); +} + +async function refreshAllProviders(): Promise { + const promises = Array.from(inlayProviders.values()).map(async (provider) => { + try { + await provider.refresh(); + } catch (error) { + logger.error("Failed to refresh provider", error); + throw error; + } + }); - await Promise.all(promises); + await Promise.all(promises); } function updateStatusBar() { - const enabled = configManager.isEnabled(); - statusBarItem.text = `$(symbol-variable) TF${enabled ? '' : ' (disabled)'}`; - statusBarItem.tooltip = enabled - ? 'Terraform Variable Resolver is active. Click to disable.' - : 'Terraform Variable Resolver is disabled. Click to enable.'; + const enabled = configManager.isEnabled(); + statusBarItem.text = `$(symbol-variable) TF${enabled ? "" : " (disabled)"}`; + statusBarItem.tooltip = enabled + ? "Terraform Variable Resolver is active. Click to disable." + : "Terraform Variable Resolver is disabled. Click to enable."; } function handleWorkspaceFolderChanges( - event: vscode.WorkspaceFoldersChangeEvent, - context: vscode.ExtensionContext + event: vscode.WorkspaceFoldersChangeEvent, + context: vscode.ExtensionContext ) { - // Handle removed folders - for (const folder of event.removed) { - const provider = inlayProviders.get(folder.uri.fsPath); - if (provider) { - provider.dispose(); - inlayProviders.delete(folder.uri.fsPath); - logger.info(`Provider disposed for removed workspace: ${folder.name}`); - } + // Handle removed folders + for (const folder of event.removed) { + const provider = inlayProviders.get(folder.uri.fsPath); + if (provider) { + provider.dispose(); + inlayProviders.delete(folder.uri.fsPath); + logger.info(`Provider disposed for removed workspace: ${folder.name}`); } + } - // Handle added folders - for (const folder of event.added) { - try { - const workspaceRoot = folder.uri.fsPath; - const provider = new TerraformInlayProvider(workspaceRoot, logger, configManager); - inlayProviders.set(workspaceRoot, provider); - - const inlayProviderDisposable = vscode.languages.registerInlayHintsProvider( - { scheme: 'file', language: 'terraform', pattern: `${workspaceRoot}/**/*.tf` }, - provider - ); - - context.subscriptions.push(inlayProviderDisposable); - logger.info(`Provider registered for new workspace: ${folder.name}`); - } catch (error) { - logger.error(`Failed to initialize provider for new workspace ${folder.name}`, error); - vscode.window.showErrorMessage(`Failed to initialize Terraform resolver for workspace ${folder.name}`); - } + // Handle added folders + for (const folder of event.added) { + try { + const workspaceRoot = folder.uri.fsPath; + const provider = new TerraformInlayProvider( + workspaceRoot, + logger, + configManager + ); + inlayProviders.set(workspaceRoot, provider); + + const inlayProviderDisposable = + vscode.languages.registerInlayHintsProvider( + { + scheme: "file", + language: "terraform", + pattern: `${workspaceRoot}/**/*.tf`, + }, + provider + ); + + context.subscriptions.push(inlayProviderDisposable); + logger.info(`Provider registered for new workspace: ${folder.name}`); + } catch (error) { + logger.error( + `Failed to initialize provider for new workspace ${folder.name}`, + error + ); + vscode.window.showErrorMessage( + `Failed to initialize Terraform resolver for workspace ${folder.name}` + ); } + } } async function showDiagnostics(): Promise { - const diagnostics: string[] = []; - - diagnostics.push('=== Terraform Variable Resolver Diagnostics ==='); - diagnostics.push(`Enabled: ${configManager.isEnabled()}`); - diagnostics.push(`Active Workspaces: ${inlayProviders.size}`); - - for (const [workspace, provider] of inlayProviders) { - diagnostics.push(`\nWorkspace: ${workspace}`); - diagnostics.push(`Cache Size: ${provider.getCacheSize()}`); - diagnostics.push(`Cache Hit Rate: ${provider.getCacheHitRate()}%`); - } + const diagnostics: string[] = []; - const doc = await vscode.workspace.openTextDocument({ - content: diagnostics.join('\n'), - language: 'plaintext' - }); - - await vscode.window.showTextDocument(doc); + diagnostics.push("=== Terraform Variable Resolver Diagnostics ==="); + diagnostics.push(`Enabled: ${configManager.isEnabled()}`); + diagnostics.push(`Active Workspaces: ${inlayProviders.size}`); + + for (const [workspace, provider] of inlayProviders) { + diagnostics.push(`\nWorkspace: ${workspace}`); + diagnostics.push(`Cache Size: ${provider.getCacheSize()}`); + diagnostics.push(`Cache Hit Rate: ${provider.getCacheHitRate()}%`); + } + + const doc = await vscode.workspace.openTextDocument({ + content: diagnostics.join("\n"), + language: "plaintext", + }); + + await vscode.window.showTextDocument(doc); } export async function deactivate(): Promise { - logger?.info('Terraform Variable Resolver deactivating...'); + logger?.info("Terraform Variable Resolver deactivating..."); - try { - // Dispose all providers - const disposePromises = Array.from(inlayProviders.values()).map(async provider => { - try { - await provider.dispose(); - } catch (error) { - logger?.error('Error disposing provider', error); - } - }); - - await Promise.all(disposePromises); - inlayProviders.clear(); - - // Dispose status bar - statusBarItem?.dispose(); - - logger?.info('Terraform Variable Resolver deactivated successfully'); - logger?.dispose(); - } catch (error) { - console.error('Error during deactivation:', error); - } -} \ No newline at end of file + try { + // Dispose all providers + const disposePromises = Array.from(inlayProviders.values()).map( + async (provider) => { + try { + await provider.dispose(); + } catch (error) { + logger?.error("Error disposing provider", error); + } + } + ); + + await Promise.all(disposePromises); + inlayProviders.clear(); + + // Dispose status bar + statusBarItem?.dispose(); + + logger?.info("Terraform Variable Resolver deactivated successfully"); + logger?.dispose(); + } catch (error) { + console.error("Error during deactivation:", error); + } +} diff --git a/src/parsers/terraformParser.ts b/src/parsers/terraformParser.ts index fa73171..8a69bb8 100644 --- a/src/parsers/terraformParser.ts +++ b/src/parsers/terraformParser.ts @@ -1,466 +1,525 @@ -import { Logger } from '../utils/logger'; +import { Logger } from "../utils/logger"; interface ModuleCall { - name: string; - source: string; - variables: { [key: string]: string }; - location: any; // vscode.Range would be imported in actual implementation + name: string; + source: string; + variables: { [key: string]: string }; + location: any; // vscode.Range would be imported in actual implementation } export class TerraformParser { - constructor(private logger: Logger) {} - - parseJsonVariable(content: string, variableName: string): string | null { - try { - const json = JSON.parse(content); - if (json[variableName] !== undefined) { - return this.formatValue(json[variableName]); - } - return null; - } catch (error) { - this.logger.error('Error parsing JSON tfvars', error); - return null; - } + constructor(private logger: Logger) {} + + parseJsonVariable(content: string, variableName: string): string | null { + try { + const json = JSON.parse(content); + if (json[variableName] !== undefined) { + return this.formatValue(json[variableName]); + } + return null; + } catch (error) { + this.logger.error("Error parsing JSON tfvars", error); + return null; } - - parseHclVariable(content: string, variableName: string): string | null { - try { - // Enhanced HCL parsing patterns with better object support - const patterns = [ - // Simple string assignment: variable_name = "value" - new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*"([^"]*)"\\s*$`, 'm'), - - // Unquoted value: variable_name = value - new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*([^\\s\\n#\\{\\[]+)\\s*(?:#.*)?$`, 'm'), - - // Array assignment: variable_name = ["value1", "value2"] - new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*(\\[[^\\]]*\\])\\s*$`, 'm'), - - // FIXED: Multi-line object assignment with proper brace matching - new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*(\\{[\\s\\S]*?\\})\\s*(?:\\n|$)`, 'gm'), - - // Boolean values - new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*(true|false)\\s*$`, 'm'), - - // Numeric values - new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*(\\d+(?:\\.\\d+)?)\\s*$`, 'm'), - - // Heredoc strings - new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*<<-?\\s*(\\w+)\\s*\\n([\\s\\S]*?)^\\s*\\1\\s*$`, 'gm') - ]; - - for (const pattern of patterns) { - const match = pattern.exec(content); - if (match) { - let value = match[1]; - - // Handle heredoc - if (match[2] !== undefined) { - value = match[2]; - } - - // FIXED: Proper object parsing with brace matching - if (value.trim().startsWith('{')) { - return this.parseComplexObject(value, content, variableName); - } - - return this.cleanValue(value); - } - } - - return null; - } catch (error) { - this.logger.error(`Error parsing HCL variable ${variableName}`, error); - return null; + } + + parseHclVariable(content: string, variableName: string): string | null { + try { + // Enhanced HCL parsing patterns with better object support + const patterns = [ + // Simple string assignment: variable_name = "value" + new RegExp( + `^\\s*${this.escapeRegex(variableName)}\\s*=\\s*"([^"]*)"\\s*$`, + "m" + ), + + // Unquoted value: variable_name = value + new RegExp( + `^\\s*${this.escapeRegex(variableName)}\\s*=\\s*([^\\s\\n#\\{\\[]+)\\s*(?:#.*)?$`, + "m" + ), + + // Array assignment: variable_name = ["value1", "value2"] + new RegExp( + `^\\s*${this.escapeRegex(variableName)}\\s*=\\s*(\\[[^\\]]*\\])\\s*$`, + "m" + ), + + // FIXED: Multi-line object assignment with proper brace matching + new RegExp( + `^\\s*${this.escapeRegex(variableName)}\\s*=\\s*(\\{[\\s\\S]*?\\})\\s*(?:\\n|$)`, + "gm" + ), + + // Boolean values + new RegExp( + `^\\s*${this.escapeRegex(variableName)}\\s*=\\s*(true|false)\\s*$`, + "m" + ), + + // Numeric values + new RegExp( + `^\\s*${this.escapeRegex(variableName)}\\s*=\\s*(\\d+(?:\\.\\d+)?)\\s*$`, + "m" + ), + + // Heredoc strings + new RegExp( + `^\\s*${this.escapeRegex(variableName)}\\s*=\\s*<<-?\\s*(\\w+)\\s*\\n([\\s\\S]*?)^\\s*\\1\\s*$`, + "gm" + ), + ]; + + for (const pattern of patterns) { + const match = pattern.exec(content); + if (match) { + let value = match[1]; + + // Handle heredoc + if (match[2] !== undefined) { + value = match[2]; + } + + // FIXED: Proper object parsing with brace matching + if (value.trim().startsWith("{")) { + return this.parseComplexObject(value, content, variableName); + } + + return this.cleanValue(value); } - } + } - // NEW: Proper complex object parsing with brace matching - private parseComplexObject(objectValue: string, fullContent: string, variableName: string): string { - try { - // If we only got the opening brace, find the complete object - if (objectValue.trim() === '{') { - return this.extractCompleteObject(fullContent, variableName); - } - - // Verify we have a complete object by counting braces - if (!this.hasMatchingBraces(objectValue)) { - return this.extractCompleteObject(fullContent, variableName); - } - - // Clean and format the object - return this.formatComplexObject(objectValue); - } catch (error) { - this.logger.error(`Error parsing complex object for ${variableName}`, error); - return objectValue; // Return as-is if parsing fails - } + return null; + } catch (error) { + this.logger.error(`Error parsing HCL variable ${variableName}`, error); + return null; } - - // NEW: Extract complete object from content using brace matching - private extractCompleteObject(content: string, variableName: string): string { - try { - const lines = content.split('\n'); - let objectStart = -1; - let braceCount = 0; - let objectLines: string[] = []; - let inObject = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Find the start of our variable assignment - if (!inObject && line.match(new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*\\{`))) { - inObject = true; - objectStart = i; - braceCount = (line.match(/\\{/g) || []).length - (line.match(/\\}/g) || []).length; - - // Extract the part after the = sign - const afterEquals = line.substring(line.indexOf('=') + 1).trim(); - objectLines.push(afterEquals); - - if (braceCount === 0) { - // Single line object - break; - } - continue; - } - - if (inObject) { - objectLines.push(line); - braceCount += (line.match(/\\{/g) || []).length - (line.match(/\\}/g) || []).length; - - if (braceCount <= 0) { - break; - } - } - } - - if (objectLines.length > 0) { - const completeObject = objectLines.join('\n'); - return this.formatComplexObject(completeObject); - } - - return '{}'; - } catch (error) { - this.logger.error(`Error extracting complete object for ${variableName}`, error); - return '{}'; - } + } + + // NEW: Proper complex object parsing with brace matching + private parseComplexObject( + objectValue: string, + fullContent: string, + variableName: string + ): string { + try { + // If we only got the opening brace, find the complete object + if (objectValue.trim() === "{") { + return this.extractCompleteObject(fullContent, variableName); + } + + // Verify we have a complete object by counting braces + if (!this.hasMatchingBraces(objectValue)) { + return this.extractCompleteObject(fullContent, variableName); + } + + // Clean and format the object + return this.formatComplexObject(objectValue); + } catch (error) { + this.logger.error( + `Error parsing complex object for ${variableName}`, + error + ); + return objectValue; // Return as-is if parsing fails } + } + + // NEW: Extract complete object from content using brace matching + private extractCompleteObject(content: string, variableName: string): string { + try { + const lines = content.split("\n"); + let objectStart = -1; + let braceCount = 0; + let objectLines: string[] = []; + let inObject = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Find the start of our variable assignment + if ( + !inObject && + line.match( + new RegExp(`^\\s*${this.escapeRegex(variableName)}\\s*=\\s*\\{`) + ) + ) { + inObject = true; + objectStart = i; + braceCount = + (line.match(/\\{/g) || []).length - + (line.match(/\\}/g) || []).length; + + // Extract the part after the = sign + const afterEquals = line.substring(line.indexOf("=") + 1).trim(); + objectLines.push(afterEquals); + + if (braceCount === 0) { + // Single line object + break; + } + continue; + } + + if (inObject) { + objectLines.push(line); + braceCount += + (line.match(/\\{/g) || []).length - + (line.match(/\\}/g) || []).length; - // NEW: Check if braces are properly matched - private hasMatchingBraces(str: string): boolean { - let braceCount = 0; - let inString = false; - let escaped = false; - - for (let i = 0; i < str.length; i++) { - const char = str[i]; - - if (escaped) { - escaped = false; - continue; - } - - if (char === '\\') { - escaped = true; - continue; - } - - if (char === '"' && !escaped) { - inString = !inString; - continue; - } - - if (!inString) { - if (char === '{') { - braceCount++; - } else if (char === '}') { - braceCount--; - } - } + if (braceCount <= 0) { + break; + } } - - return braceCount === 0; + } + + if (objectLines.length > 0) { + const completeObject = objectLines.join("\n"); + return this.formatComplexObject(completeObject); + } + + return "{}"; + } catch (error) { + this.logger.error( + `Error extracting complete object for ${variableName}`, + error + ); + return "{}"; } - - // NEW: Format complex object for better display - private formatComplexObject(objectStr: string): string { - try { - const trimmed = objectStr.trim(); - - // Try to convert to JSON for consistent formatting - const jsonStr = this.convertHclObjectToJson(trimmed); - const parsed = JSON.parse(jsonStr); - return JSON.stringify(parsed, null, 2); - } catch (error) { - // If JSON conversion fails, format as HCL - return this.formatHclObject(objectStr); + } + + // NEW: Check if braces are properly matched + private hasMatchingBraces(str: string): boolean { + let braceCount = 0; + let inString = false; + let escaped = false; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + + if (escaped) { + escaped = false; + continue; + } + + if (char === "\\") { + escaped = true; + continue; + } + + if (char === '"' && !escaped) { + inString = !inString; + continue; + } + + if (!inString) { + if (char === "{") { + braceCount++; + } else if (char === "}") { + braceCount--; } + } } - // NEW: Convert HCL object syntax to JSON - private convertHclObjectToJson(hclObj: string): string { - let jsonStr = hclObj.trim(); - - if (!jsonStr.startsWith('{')) { - jsonStr = '{' + jsonStr + '}'; - } - - // Convert HCL syntax to JSON - jsonStr = jsonStr - // Convert key = value to "key": value - .replace(/(\w+)\s*=\s*/g, '"$1": ') - // Ensure string values are quoted - .replace(/:\s*([^",\\{\\[\\n\\r]+)(\s*[,\\}\\n\\r])/g, (match, value, suffix) => { - const trimmedValue = value.trim(); - if (trimmedValue === 'true' || trimmedValue === 'false' || /^\\d+(\\.\\d+)?$/.test(trimmedValue)) { - return `: ${trimmedValue}${suffix}`; - } - return `: "${trimmedValue}"${suffix}`; - }) - // Fix trailing commas - .replace(/,(\s*[\\}\\]])/g, '$1'); - - return jsonStr; + return braceCount === 0; + } + + // NEW: Format complex object for better display + private formatComplexObject(objectStr: string): string { + try { + const trimmed = objectStr.trim(); + + // Try to convert to JSON for consistent formatting + const jsonStr = this.convertHclObjectToJson(trimmed); + const parsed = JSON.parse(jsonStr); + return JSON.stringify(parsed, null, 2); + } catch (error) { + // If JSON conversion fails, format as HCL + return this.formatHclObject(objectStr); } + } - // NEW: Format HCL object when JSON conversion fails - private formatHclObject(hclObj: string): string { - const lines = hclObj.split('\n'); - let indentLevel = 0; - const formatted: string[] = []; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - - if (trimmed.includes('}')) { - indentLevel = Math.max(0, indentLevel - 1); - } - - formatted.push(' '.repeat(indentLevel) + trimmed); - - if (trimmed.includes('{')) { - indentLevel++; - } - } - - return formatted.join('\n'); + // NEW: Convert HCL object syntax to JSON + private convertHclObjectToJson(hclObj: string): string { + let jsonStr = hclObj.trim(); + + if (!jsonStr.startsWith("{")) { + jsonStr = "{" + jsonStr + "}"; } - parseLocalsBlock(content: string, variableName: string): string | null { - try { - // Match locals blocks - const localsRegex = /locals\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs; - let match; - - while ((match = localsRegex.exec(content)) !== null) { - const localsContent = match[1]; - const value = this.parseHclVariable(localsContent, variableName); - if (value !== null) { - return value; - } - } - - return null; - } catch (error) { - this.logger.error(`Error parsing locals block for ${variableName}`, error); - return null; + // Convert HCL syntax to JSON + jsonStr = jsonStr + // Convert key = value to "key": value + .replace(/(\w+)\s*=\s*/g, '"$1": ') + // Ensure string values are quoted + .replace( + /:\s*([^",\\{\\[\\n\\r]+)(\s*[,\\}\\n\\r])/g, + (match, value, suffix) => { + const trimmedValue = value.trim(); + if ( + trimmedValue === "true" || + trimmedValue === "false" || + /^\\d+(\\.\\d+)?$/.test(trimmedValue) + ) { + return `: ${trimmedValue}${suffix}`; + } + return `: "${trimmedValue}"${suffix}`; } + ) + // Fix trailing commas + .replace(/,(\s*[\\}\\]])/g, "$1"); + + return jsonStr; + } + + // NEW: Format HCL object when JSON conversion fails + private formatHclObject(hclObj: string): string { + const lines = hclObj.split("\n"); + let indentLevel = 0; + const formatted: string[] = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + if (trimmed.includes("}")) { + indentLevel = Math.max(0, indentLevel - 1); + } + + formatted.push(" ".repeat(indentLevel) + trimmed); + + if (trimmed.includes("{")) { + indentLevel++; + } } - parseOutputBlock(content: string, outputName: string): string | null { - try { - // Match output blocks with the specific name - const outputRegex = new RegExp( - `output\\s+"${this.escapeRegex(outputName)}"\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, - 'gs' - ); - - const match = outputRegex.exec(content); - if (match) { - const outputContent = match[1]; - - // Extract the value from the output block - const valueMatch = /value\s*=\s*([^\n]*)/g.exec(outputContent); - if (valueMatch) { - return this.cleanValue(valueMatch[1]); - } - - // Handle multi-line values - const multiLineValueMatch = /value\s*=\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs.exec(outputContent); - if (multiLineValueMatch) { - return `{${multiLineValueMatch[1]}}`; - } - } - - return null; - } catch (error) { - this.logger.error(`Error parsing output block for ${outputName}`, error); - return null; + return formatted.join("\n"); + } + + parseLocalsBlock(content: string, variableName: string): string | null { + try { + // Match locals blocks + const localsRegex = /locals\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs; + let match; + + while ((match = localsRegex.exec(content)) !== null) { + const localsContent = match[1]; + const value = this.parseHclVariable(localsContent, variableName); + if (value !== null) { + return value; } + } + + return null; + } catch (error) { + this.logger.error( + `Error parsing locals block for ${variableName}`, + error + ); + return null; } + } + + parseOutputBlock(content: string, outputName: string): string | null { + try { + // Match output blocks with the specific name + const outputRegex = new RegExp( + `output\\s+"${this.escapeRegex(outputName)}"\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, + "gs" + ); + + const match = outputRegex.exec(content); + if (match) { + const outputContent = match[1]; + + // Extract the value from the output block + const valueMatch = /value\s*=\s*([^\n]*)/g.exec(outputContent); + if (valueMatch) { + return this.cleanValue(valueMatch[1]); + } - parseModuleBlock(content: string, moduleName: string): ModuleCall | null { - try { - const moduleRegex = new RegExp( - `module\\s+"${this.escapeRegex(moduleName)}"\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, - 'gs' - ); - - const match = moduleRegex.exec(content); - if (match) { - const moduleContent = match[1]; - - // Extract source - const sourceMatch = /source\s*=\s*"([^"]*)"/.exec(moduleContent); - if (!sourceMatch) { - return null; - } - - return { - name: moduleName, - source: sourceMatch[1], - variables: this.parseModuleVariables(moduleContent), - location: null // Would be calculated in actual implementation - }; - } - - return null; - } catch (error) { - this.logger.error(`Error parsing module block for ${moduleName}`, error); - return null; + // Handle multi-line values + const multiLineValueMatch = + /value\s*=\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs.exec(outputContent); + if (multiLineValueMatch) { + return `{${multiLineValueMatch[1]}}`; } - } + } - extractVariableReferences(value: string): string[] { - try { - const references: string[] = []; - const patterns = [ - /\bvar\.(\w+)/g, - /\blocal\.(\w+)/g, - /\bmodule\.([\w.]+)/g, - /\bdata\.([\w.]+)\.([\w.]+)/g - ]; - - for (const pattern of patterns) { - let match; - pattern.lastIndex = 0; // Reset regex state - - while ((match = pattern.exec(value)) !== null) { - if (pattern.source.includes('data')) { - references.push(`data.${match[1]}.${match[2]}`); - } else { - references.push(match[0]); - } - } - } - - return [...new Set(references)]; // Remove duplicates - } catch (error) { - this.logger.error('Error extracting variable references', error); - return []; + return null; + } catch (error) { + this.logger.error(`Error parsing output block for ${outputName}`, error); + return null; + } + } + + parseModuleBlock(content: string, moduleName: string): ModuleCall | null { + try { + const moduleRegex = new RegExp( + `module\\s+"${this.escapeRegex(moduleName)}"\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, + "gs" + ); + + const match = moduleRegex.exec(content); + if (match) { + const moduleContent = match[1]; + + // Extract source + const sourceMatch = /source\s*=\s*"([^"]*)"/.exec(moduleContent); + if (!sourceMatch) { + return null; } + + return { + name: moduleName, + source: sourceMatch[1], + variables: this.parseModuleVariables(moduleContent), + location: null, // Would be calculated in actual implementation + }; + } + + return null; + } catch (error) { + this.logger.error(`Error parsing module block for ${moduleName}`, error); + return null; } + } + + extractVariableReferences(value: string): string[] { + try { + const references: string[] = []; + const patterns = [ + /\bvar\.(\w+)/g, + /\blocal\.(\w+)/g, + /\bmodule\.([\w.]+)/g, + /\bdata\.([\w.]+)\.([\w.]+)/g, + ]; + + for (const pattern of patterns) { + let match; + pattern.lastIndex = 0; // Reset regex state + + while ((match = pattern.exec(value)) !== null) { + if (pattern.source.includes("data")) { + references.push(`data.${match[1]}.${match[2]}`); + } else { + references.push(match[0]); + } + } + } - parseModuleVariables(moduleContent: string): { [key: string]: string } { - const variables: { [key: string]: string } = {}; - - try { - const lines = moduleContent.split('\n'); - - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine.startsWith('source') || trimmedLine.startsWith('#') || !trimmedLine.includes('=')) { - continue; - } - - const match = /^\s*(\w+)\s*=\s*(.+)$/.exec(trimmedLine); - if (match) { - const key = match[1]; - const value = this.cleanValue(match[2]); - variables[key] = value; - } - } - } catch (error) { - this.logger.error('Error parsing module variables', error); + return [...new Set(references)]; // Remove duplicates + } catch (error) { + this.logger.error("Error extracting variable references", error); + return []; + } + } + + parseModuleVariables(moduleContent: string): { [key: string]: string } { + const variables: { [key: string]: string } = {}; + + try { + const lines = moduleContent.split("\n"); + + for (const line of lines) { + const trimmedLine = line.trim(); + if ( + trimmedLine.startsWith("source") || + trimmedLine.startsWith("#") || + !trimmedLine.includes("=") + ) { + continue; } - return variables; + const match = /^\s*(\w+)\s*=\s*(.+)$/.exec(trimmedLine); + if (match) { + const key = match[1]; + const value = this.cleanValue(match[2]); + variables[key] = value; + } + } + } catch (error) { + this.logger.error("Error parsing module variables", error); } - private cleanValue(value: string): string { - if (!value) return ''; + return variables; + } - let cleaned = value.trim(); - - // Remove trailing comments - const commentIndex = cleaned.indexOf('#'); - if (commentIndex !== -1) { - cleaned = cleaned.substring(0, commentIndex).trim(); - } + private cleanValue(value: string): string { + if (!value) return ""; - // Remove trailing commas - if (cleaned.endsWith(',')) { - cleaned = cleaned.slice(0, -1).trim(); - } + let cleaned = value.trim(); - return cleaned; + // Remove trailing comments + const commentIndex = cleaned.indexOf("#"); + if (commentIndex !== -1) { + cleaned = cleaned.substring(0, commentIndex).trim(); } - private formatValue(value: any): string { - if (value === null || value === undefined) { - return 'null'; - } + // Remove trailing commas + if (cleaned.endsWith(",")) { + cleaned = cleaned.slice(0, -1).trim(); + } - if (typeof value === 'string') { - return `"${value}"`; - } + return cleaned; + } - if (typeof value === 'boolean') { - return value.toString(); - } + private formatValue(value: any): string { + if (value === null || value === undefined) { + return "null"; + } - if (typeof value === 'number') { - return value.toString(); - } + if (typeof value === "string") { + return `"${value}"`; + } - if (Array.isArray(value)) { - if (value.length === 0) { - return '[]'; - } - - if (value.length > 3) { - return `[${value.slice(0, 3).map(v => this.formatValue(v)).join(', ')}, ...]`; - } - - return `[${value.map(v => this.formatValue(v)).join(', ')}]`; - } + if (typeof value === "boolean") { + return value.toString(); + } - if (typeof value === 'object') { - const entries = Object.entries(value); - - if (entries.length === 0) { - return '{}'; - } - - if (entries.length > 3) { - const preview = entries.slice(0, 2) - .map(([k, v]) => `${k} = ${this.formatValue(v)}`) - .join(', '); - return `{ ${preview}, ... }`; - } - - const formatted = entries - .map(([k, v]) => `${k} = ${this.formatValue(v)}`) - .join(', '); - return `{ ${formatted} }`; - } + if (typeof value === "number") { + return value.toString(); + } - return String(value); + if (Array.isArray(value)) { + if (value.length === 0) { + return "[]"; + } + + if (value.length > 3) { + return `[${value + .slice(0, 3) + .map((v) => this.formatValue(v)) + .join(", ")}, ...]`; + } + + return `[${value.map((v) => this.formatValue(v)).join(", ")}]`; } - private escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (typeof value === "object") { + const entries = Object.entries(value); + + if (entries.length === 0) { + return "{}"; + } + + if (entries.length > 3) { + const preview = entries + .slice(0, 2) + .map(([k, v]) => `${k} = ${this.formatValue(v)}`) + .join(", "); + return `{ ${preview}, ... }`; + } + + const formatted = entries + .map(([k, v]) => `${k} = ${this.formatValue(v)}`) + .join(", "); + return `{ ${formatted} }`; } -} \ No newline at end of file + + return String(value); + } + + private escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } +} diff --git a/src/providers/inlayProvider.ts b/src/providers/inlayProvider.ts index f34e8e5..033f86c 100644 --- a/src/providers/inlayProvider.ts +++ b/src/providers/inlayProvider.ts @@ -1,638 +1,746 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; -import { TerraformVariableResolver } from '../resolvers/variableResolver'; -import { Logger } from '../utils/logger'; -import { ConfigurationManager } from '../utils/configurationManager'; -import { PerformanceMonitor } from '../utils/performanceMonitor'; +import * as path from "path"; +import * as vscode from "vscode"; + +import { TerraformVariableResolver } from "../resolvers/variableResolver"; +import { ConfigurationManager } from "../utils/configurationManager"; +import { Logger } from "../utils/logger"; +import { PerformanceMonitor } from "../utils/performanceMonitor"; interface InlayHintWithPosition { - hint: vscode.InlayHint; - variableName: string; - resolvedValue: string; - position: vscode.Position; + hint: vscode.InlayHint; + variableName: string; + resolvedValue: string; + position: vscode.Position; } export class TerraformInlayProvider implements vscode.InlayHintsProvider { - private resolver: TerraformVariableResolver; - private disposables: vscode.Disposable[] = []; - private performanceMonitor: PerformanceMonitor; - private cacheHits = 0; - private cacheMisses = 0; - - constructor( - private workspaceRoot: string, - private logger: Logger, - private configManager: ConfigurationManager - ) { - this.resolver = new TerraformVariableResolver(workspaceRoot, logger); - this.performanceMonitor = new PerformanceMonitor(logger); - this.logger.info(`TerraformInlayProvider initialized for workspace: ${workspaceRoot}`); - } - - async dispose(): Promise { - this.logger.info('Disposing TerraformInlayProvider...'); - - try { - await this.resolver.dispose(); - this.disposables.forEach(d => d.dispose()); - this.disposables = []; - this.performanceMonitor.dispose(); - } catch (error) { - this.logger.error('Error during provider disposal', error); - } + private resolver: TerraformVariableResolver; + private disposables: vscode.Disposable[] = []; + private performanceMonitor: PerformanceMonitor; + private cacheHits = 0; + private cacheMisses = 0; + + constructor( + private workspaceRoot: string, + private logger: Logger, + private configManager: ConfigurationManager + ) { + this.resolver = new TerraformVariableResolver(workspaceRoot, logger); + this.performanceMonitor = new PerformanceMonitor(logger); + this.logger.info( + `TerraformInlayProvider initialized for workspace: ${workspaceRoot}` + ); + } + + async dispose(): Promise { + this.logger.info("Disposing TerraformInlayProvider..."); + + try { + await this.resolver.dispose(); + this.disposables.forEach((d) => d.dispose()); + this.disposables = []; + this.performanceMonitor.dispose(); + } catch (error) { + this.logger.error("Error during provider disposal", error); } - - async refresh(): Promise { - this.logger.info('Refreshing provider...'); - try { - await this.resolver.clearCache(); - this.cacheHits = 0; - this.cacheMisses = 0; - this.logger.info('Provider refreshed successfully'); - } catch (error) { - this.logger.error('Failed to refresh provider', error); - throw error; - } + } + + async refresh(): Promise { + this.logger.info("Refreshing provider..."); + try { + await this.resolver.clearCache(); + this.cacheHits = 0; + this.cacheMisses = 0; + this.logger.info("Provider refreshed successfully"); + } catch (error) { + this.logger.error("Failed to refresh provider", error); + throw error; } - - clearCache(): void { - this.resolver.clearCache(); - this.cacheHits = 0; - this.cacheMisses = 0; + } + + clearCache(): void { + this.resolver.clearCache(); + this.cacheHits = 0; + this.cacheMisses = 0; + } + + getCacheSize(): number { + return this.resolver.getCacheSize(); + } + + getCacheHitRate(): number { + const total = this.cacheHits + this.cacheMisses; + return total > 0 ? Math.round((this.cacheHits / total) * 100) : 0; + } + + async provideInlayHints( + document: vscode.TextDocument, + range: vscode.Range, + token: vscode.CancellationToken + ): Promise { + if (!this.configManager.isEnabled()) { + return []; } - getCacheSize(): number { - return this.resolver.getCacheSize(); + if (!document.fileName.endsWith(".tf")) { + return []; } - getCacheHitRate(): number { - const total = this.cacheHits + this.cacheMisses; - return total > 0 ? Math.round((this.cacheHits / total) * 100) : 0; + const stopwatch = this.performanceMonitor.startTimer( + `provideInlayHints-${path.basename(document.fileName)}` + ); + + try { + const hints = await this.generateInlayHints(document, range, token); + stopwatch.stop(); + + this.logger.debug( + `Generated ${hints.length} hints for ${document.fileName} in ${stopwatch.getDuration()}ms` + ); + return hints; + } catch (error) { + stopwatch.stop(); + this.logger.error( + `Failed to provide inlay hints for ${document.fileName}`, + error + ); + return []; } - - async provideInlayHints( - document: vscode.TextDocument, - range: vscode.Range, - token: vscode.CancellationToken - ): Promise { - if (!this.configManager.isEnabled()) { - return []; - } - - if (!document.fileName.endsWith('.tf')) { - return []; + } + + private async generateInlayHints( + document: vscode.TextDocument, + range: vscode.Range, + token: vscode.CancellationToken + ): Promise { + const hints: vscode.InlayHint[] = []; + const text = document.getText(range); + const currentDir = path.dirname(document.fileName); + const processedPositions = new Set(); + + // Enhanced patterns for better variable detection + const patterns = [ + { + regex: /\bvar\.(\w+)\.(\w+)/g, // NEW: var.object.property access + type: "variable_property" as const, + prefix: "var.", + }, + { + regex: /\bvar\.(\w+)/g, + type: "variable" as const, + prefix: "var.", + }, + { + regex: /\blocal\.(\w+)\.(\w+)/g, // NEW: local.object.property access + type: "local_property" as const, + prefix: "local.", + }, + { + regex: /\blocal\.(\w+)/g, + type: "local" as const, + prefix: "local.", + }, + { + regex: /\bmodule\.([\w.]+)/g, + type: "module" as const, + prefix: "module.", + }, + { + regex: /\bdata\.([\w.]+)\.([\w.]+)/g, + type: "data" as const, + prefix: "data.", + }, + ]; + + for (const pattern of patterns) { + let match; + pattern.regex.lastIndex = 0; // Reset regex state + + while ((match = pattern.regex.exec(text)) !== null) { + if (token.isCancellationRequested) { + this.logger.debug("Inlay hint generation cancelled"); + return hints; } - const stopwatch = this.performanceMonitor.startTimer(`provideInlayHints-${path.basename(document.fileName)}`); - try { - const hints = await this.generateInlayHints(document, range, token); - stopwatch.stop(); - - this.logger.debug(`Generated ${hints.length} hints for ${document.fileName} in ${stopwatch.getDuration()}ms`); - return hints; + let variableName: string; + let propertyName: string | null = null; + + // FIXED: Handle object property access + if ( + pattern.type === "variable_property" || + pattern.type === "local_property" + ) { + variableName = match[1]; // The object name + propertyName = match[2]; // The property being accessed + } else { + variableName = match[1] || match[0]; + } + + const fullMatch = match[0]; + const matchStart = range.start.character + match.index; + const matchEnd = matchStart + fullMatch.length; + + // Calculate precise position at the end of the variable reference + const endPosition = document.positionAt( + document.offsetAt(range.start) + match.index + fullMatch.length + ); + + // Avoid duplicate hints at the same position + const positionKey = `${endPosition.line}:${endPosition.character}:${fullMatch}`; + if (processedPositions.has(positionKey)) { + continue; + } + processedPositions.add(positionKey); + + // Ensure we're not inside a string or comment + if (this.isInStringOrComment(document, endPosition)) { + continue; + } + + // FIXED: Resolve with property access support + const allResolvedValues = + await this.resolveVariableAllContextsWithProperty( + variableName, + propertyName, + currentDir, + pattern.type + ); + + if (allResolvedValues.length > 0) { + const hint = this.createInlayHintWithMultipleValues( + endPosition, + allResolvedValues, + propertyName ? `${variableName}.${propertyName}` : variableName + ); + hints.push(hint); + } } catch (error) { - stopwatch.stop(); - this.logger.error(`Failed to provide inlay hints for ${document.fileName}`, error); - return []; + this.logger.error( + `Error processing variable match: ${match[0]}`, + error + ); + continue; // Continue with other matches } + } } - private async generateInlayHints( - document: vscode.TextDocument, - range: vscode.Range, - token: vscode.CancellationToken - ): Promise { - const hints: vscode.InlayHint[] = []; - const text = document.getText(range); - const currentDir = path.dirname(document.fileName); - const processedPositions = new Set(); - - // Enhanced patterns for better variable detection - const patterns = [ - { - regex: /\bvar\.(\w+)\.(\w+)/g, // NEW: var.object.property access - type: 'variable_property' as const, - prefix: 'var.' - }, - { - regex: /\bvar\.(\w+)/g, - type: 'variable' as const, - prefix: 'var.' - }, - { - regex: /\blocal\.(\w+)\.(\w+)/g, // NEW: local.object.property access - type: 'local_property' as const, - prefix: 'local.' - }, - { - regex: /\blocal\.(\w+)/g, - type: 'local' as const, - prefix: 'local.' - }, - { - regex: /\bmodule\.([\w.]+)/g, - type: 'module' as const, - prefix: 'module.' - }, - { - regex: /\bdata\.([\w.]+)\.([\w.]+)/g, - type: 'data' as const, - prefix: 'data.' - } - ]; - - for (const pattern of patterns) { - let match; - pattern.regex.lastIndex = 0; // Reset regex state - - while ((match = pattern.regex.exec(text)) !== null) { - if (token.isCancellationRequested) { - this.logger.debug('Inlay hint generation cancelled'); - return hints; - } - - try { - let variableName: string; - let propertyName: string | null = null; - - // FIXED: Handle object property access - if (pattern.type === 'variable_property' || pattern.type === 'local_property') { - variableName = match[1]; // The object name - propertyName = match[2]; // The property being accessed - } else { - variableName = match[1] || match[0]; - } - - const fullMatch = match[0]; - const matchStart = range.start.character + match.index; - const matchEnd = matchStart + fullMatch.length; - - // Calculate precise position at the end of the variable reference - const endPosition = document.positionAt( - document.offsetAt(range.start) + match.index + fullMatch.length - ); - - // Avoid duplicate hints at the same position - const positionKey = `${endPosition.line}:${endPosition.character}:${fullMatch}`; - if (processedPositions.has(positionKey)) { - continue; - } - processedPositions.add(positionKey); - - // Ensure we're not inside a string or comment - if (this.isInStringOrComment(document, endPosition)) { - continue; - } - - // FIXED: Resolve with property access support - const allResolvedValues = await this.resolveVariableAllContextsWithProperty( - variableName, - propertyName, - currentDir, - pattern.type - ); - - if (allResolvedValues.length > 0) { - const hint = this.createInlayHintWithMultipleValues( - endPosition, - allResolvedValues, - propertyName ? `${variableName}.${propertyName}` : variableName - ); - hints.push(hint); - } - } catch (error) { - this.logger.error(`Error processing variable match: ${match[0]}`, error); - continue; // Continue with other matches - } - } - } - - return hints; + return hints; + } + + private async resolveVariableAllContextsWithProperty( + variableName: string, + propertyName: string | null, + currentDir: string, + type: string + ): Promise> { + const resolvedValues: Array<{ + value: string; + context: string; + source: string; + }> = []; + const searchPaths = new Set(); + + // Add current directory + searchPaths.add(currentDir); + + // Add workspace root and common environment directories + const workspaceRoot = this.workspaceRoot; + searchPaths.add(workspaceRoot); + + // Look for common environment patterns + const commonEnvPaths = [ + "environments/dev", + "environments/test", + "environments/production", + "environments/staging", + "env/dev", + "env/test", + "env/production", + "env/staging", + "dev", + "test", + "production", + "staging", + ]; + + for (const envPath of commonEnvPaths) { + const fullPath = path.join(workspaceRoot, envPath); + searchPaths.add(fullPath); } - private async resolveVariableAllContextsWithProperty( - variableName: string, - propertyName: string | null, - currentDir: string, - type: string - ): Promise> { - const resolvedValues: Array<{value: string, context: string, source: string}> = []; - const searchPaths = new Set(); - - // Add current directory - searchPaths.add(currentDir); - - // Add workspace root and common environment directories - const workspaceRoot = this.workspaceRoot; - searchPaths.add(workspaceRoot); - - // Look for common environment patterns - const commonEnvPaths = [ - 'environments/dev', 'environments/test', 'environments/production', 'environments/staging', - 'env/dev', 'env/test', 'env/production', 'env/staging', - 'dev', 'test', 'production', 'staging' - ]; - - for (const envPath of commonEnvPaths) { - const fullPath = path.join(workspaceRoot, envPath); - searchPaths.add(fullPath); + // Search in all paths + for (const searchPath of searchPaths) { + try { + let resolvedValue: string | null = null; + + if (propertyName) { + // FIXED: Resolve object property access + resolvedValue = await this.resolveObjectProperty( + variableName, + propertyName, + searchPath + ); + } else { + // Regular variable resolution with enhanced recursion + resolvedValue = await this.resolver.resolveVariableValueEnhanced( + variableName, + searchPath + ); } - // Search in all paths - for (const searchPath of searchPaths) { - try { - let resolvedValue: string | null = null; - - if (propertyName) { - // FIXED: Resolve object property access - resolvedValue = await this.resolveObjectProperty(variableName, propertyName, searchPath); - } else { - // Regular variable resolution with enhanced recursion - resolvedValue = await this.resolver.resolveVariableValueEnhanced(variableName, searchPath); - } - - if (resolvedValue && resolvedValue.trim() !== '') { - const contextName = this.getContextName(searchPath, workspaceRoot); - const existingValue = resolvedValues.find(rv => rv.value === resolvedValue); - - if (existingValue) { - // Same value, different context - merge contexts - existingValue.context += `, ${contextName}`; - } else { - resolvedValues.push({ - value: resolvedValue, - context: contextName, - source: searchPath - }); - } - } - } catch (error) { - this.logger.debug(`Could not resolve ${variableName}${propertyName ? '.' + propertyName : ''} in ${searchPath}`); - } + if (resolvedValue && resolvedValue.trim() !== "") { + const contextName = this.getContextName(searchPath, workspaceRoot); + const existingValue = resolvedValues.find( + (rv) => rv.value === resolvedValue + ); + + if (existingValue) { + // Same value, different context - merge contexts + existingValue.context += `, ${contextName}`; + } else { + resolvedValues.push({ + value: resolvedValue, + context: contextName, + source: searchPath, + }); + } } - - return resolvedValues; + } catch (error) { + this.logger.debug( + `Could not resolve ${variableName}${propertyName ? "." + propertyName : ""} in ${searchPath}` + ); + } } - // NEW: Resolve object property access - private async resolveObjectProperty( - objectName: string, - propertyName: string, - searchPath: string - ): Promise { - try { - // First resolve the object - const objectValue = await this.resolver.resolveVariableValueEnhanced(objectName, searchPath); - - if (!objectValue) { - return null; - } - - // Parse the object and extract the property - return this.extractPropertyFromObject(objectValue, propertyName); - } catch (error) { - this.logger.error(`Error resolving object property ${objectName}.${propertyName}`, error); - return null; - } + return resolvedValues; + } + + // NEW: Resolve object property access + private async resolveObjectProperty( + objectName: string, + propertyName: string, + searchPath: string + ): Promise { + try { + // First resolve the object + const objectValue = await this.resolver.resolveVariableValueEnhanced( + objectName, + searchPath + ); + + if (!objectValue) { + return null; + } + + // Parse the object and extract the property + return this.extractPropertyFromObject(objectValue, propertyName); + } catch (error) { + this.logger.error( + `Error resolving object property ${objectName}.${propertyName}`, + error + ); + return null; } - - // NEW: Extract property from resolved object - private extractPropertyFromObject(objectValue: string, propertyName: string): string | null { - try { - const trimmed = objectValue.trim(); - - if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) { - return null; - } - - // Try JSON parsing first - try { - const parsed = JSON.parse(trimmed); - if (parsed[propertyName] !== undefined) { - return typeof parsed[propertyName] === 'string' - ? `"${parsed[propertyName]}"` - : String(parsed[propertyName]); - } - } catch { - // Not JSON, try HCL parsing - } - - // HCL property extraction - const propertyRegex = new RegExp(`${this.escapeRegex(propertyName)}\\s*=\\s*"([^"]*)"`, 'i'); - const quotedMatch = propertyRegex.exec(trimmed); - if (quotedMatch) { - return `"${quotedMatch[1]}"`; - } - - // Unquoted values - const unquotedRegex = new RegExp(`${this.escapeRegex(propertyName)}\\s*=\\s*([^\\s,}]+)`, 'i'); - const unquotedMatch = unquotedRegex.exec(trimmed); - if (unquotedMatch) { - return unquotedMatch[1]; - } - - return null; - } catch (error) { - this.logger.error(`Error extracting property ${propertyName} from object`, error); - return null; + } + + // NEW: Extract property from resolved object + private extractPropertyFromObject( + objectValue: string, + propertyName: string + ): string | null { + try { + const trimmed = objectValue.trim(); + + if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) { + return null; + } + + // Try JSON parsing first + try { + const parsed = JSON.parse(trimmed); + if (parsed[propertyName] !== undefined) { + return typeof parsed[propertyName] === "string" + ? `"${parsed[propertyName]}"` + : String(parsed[propertyName]); } + } catch { + // Not JSON, try HCL parsing + } + + // HCL property extraction + const propertyRegex = new RegExp( + `${this.escapeRegex(propertyName)}\\s*=\\s*"([^"]*)"`, + "i" + ); + const quotedMatch = propertyRegex.exec(trimmed); + if (quotedMatch) { + return `"${quotedMatch[1]}"`; + } + + // Unquoted values + const unquotedRegex = new RegExp( + `${this.escapeRegex(propertyName)}\\s*=\\s*([^\\s,}]+)`, + "i" + ); + const unquotedMatch = unquotedRegex.exec(trimmed); + if (unquotedMatch) { + return unquotedMatch[1]; + } + + return null; + } catch (error) { + this.logger.error( + `Error extracting property ${propertyName} from object`, + error + ); + return null; } + } - private escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } + private escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } - // NEW: Get context name from path - private getContextName(searchPath: string, workspaceRoot: string): string { - const relativePath = path.relative(workspaceRoot, searchPath); - if (relativePath === '' || relativePath === '.') { - return 'root'; - } - - const parts = relativePath.split(path.sep); - const lastPart = parts[parts.length - 1]; - - // Check for environment patterns - if (['dev', 'test', 'prod', 'staging'].includes(lastPart)) { - return lastPart; - } - - return relativePath; + // NEW: Get context name from path + private getContextName(searchPath: string, workspaceRoot: string): string { + const relativePath = path.relative(workspaceRoot, searchPath); + if (relativePath === "" || relativePath === ".") { + return "root"; } - private async resolveVariableWithCache( - variableName: string, - currentDir: string, - type: 'variable' | 'local' | 'module' | 'data' - ): Promise { - const cacheKey = `${type}:${variableName}:${currentDir}`; - - try { - const cached = this.resolver.getFromCache(cacheKey); - if (cached !== null) { - this.cacheHits++; - return cached; - } - - this.cacheMisses++; - const resolved = await this.resolver.resolveVariableValue(variableName, currentDir); - - if (resolved !== null) { - this.resolver.setCache(cacheKey, resolved); - } - - return resolved; - } catch (error) { - this.logger.error(`Failed to resolve variable ${variableName}`, error); - return null; - } - } + const parts = relativePath.split(path.sep); + const lastPart = parts[parts.length - 1]; - private formatResolvedValue(value: string): string { - if (!value || value.trim() === '') { - return ''; - } + // Check for environment patterns + if (["dev", "test", "prod", "staging"].includes(lastPart)) { + return lastPart; + } - // Handle complex objects - if (value.trim().startsWith('{') && value.trim().endsWith('}')) { - const content = value.trim().slice(1, -1).trim(); - if (content.length > 50 || content.includes('\n')) { - return 'complex object'; - } - return `{ ${content} }`; - } + return relativePath; + } + + private async resolveVariableWithCache( + variableName: string, + currentDir: string, + type: "variable" | "local" | "module" | "data" + ): Promise { + const cacheKey = `${type}:${variableName}:${currentDir}`; + + try { + const cached = this.resolver.getFromCache(cacheKey); + if (cached !== null) { + this.cacheHits++; + return cached; + } + + this.cacheMisses++; + const resolved = await this.resolver.resolveVariableValue( + variableName, + currentDir + ); + + if (resolved !== null) { + this.resolver.setCache(cacheKey, resolved); + } + + return resolved; + } catch (error) { + this.logger.error(`Failed to resolve variable ${variableName}`, error); + return null; + } + } - // Handle arrays - if (value.trim().startsWith('[') && value.trim().endsWith(']')) { - const content = value.trim().slice(1, -1).trim(); - if (content.length > 50 || content.includes('\n')) { - return 'array'; - } - return `[${content}]`; - } + private formatResolvedValue(value: string): string { + if (!value || value.trim() === "") { + return ""; + } - // Handle long strings - if (value.length > 80) { - return `"${value.substring(0, 75)}..."`; - } + // Handle complex objects + if (value.trim().startsWith("{") && value.trim().endsWith("}")) { + const content = value.trim().slice(1, -1).trim(); + if (content.length > 50 || content.includes("\n")) { + return "complex object"; + } + return `{ ${content} }`; + } - // Handle strings without quotes - if (!value.startsWith('"') && !value.startsWith("'") && - !value.match(/^[\d.]+$/) && !value.match(/^(true|false)$/)) { - return `"${value}"`; - } + // Handle arrays + if (value.trim().startsWith("[") && value.trim().endsWith("]")) { + const content = value.trim().slice(1, -1).trim(); + if (content.length > 50 || content.includes("\n")) { + return "array"; + } + return `[${content}]`; + } - return value; + // Handle long strings + if (value.length > 80) { + return `"${value.substring(0, 75)}..."`; } - private createInlayHintWithMultipleValues( - position: vscode.Position, - resolvedValues: Array<{value: string, context: string, source: string}>, - variableName: string - ): vscode.InlayHint { - let displayText: string; - let tooltipContent: string; - - if (resolvedValues.length === 1) { - const singleValue = resolvedValues[0]; - const formattedValue = this.formatResolvedValue(singleValue.value); - - // Check if it's a complex object - if (this.isComplexObject(singleValue.value)) { - displayText = ' → complex object'; - tooltipContent = this.createComplexObjectTooltip(variableName, singleValue.value, singleValue.context); - } else { - displayText = ` → ${formattedValue}`; - tooltipContent = this.createSingleValueTooltip(variableName, formattedValue, singleValue.context); - } - } else { - // Multiple values - displayText = ' → multiple values'; - tooltipContent = this.createMultipleValuesTooltip(variableName, resolvedValues); - } + // Handle strings without quotes + if ( + !value.startsWith('"') && + !value.startsWith("'") && + !value.match(/^[\d.]+$/) && + !value.match(/^(true|false)$/) + ) { + return `"${value}"`; + } - const hint = new vscode.InlayHint( - position, - displayText, - vscode.InlayHintKind.Parameter + return value; + } + + private createInlayHintWithMultipleValues( + position: vscode.Position, + resolvedValues: Array<{ value: string; context: string; source: string }>, + variableName: string + ): vscode.InlayHint { + let displayText: string; + let tooltipContent: string; + + if (resolvedValues.length === 1) { + const singleValue = resolvedValues[0]; + const formattedValue = this.formatResolvedValue(singleValue.value); + + // Check if it's a complex object + if (this.isComplexObject(singleValue.value)) { + displayText = " → complex object"; + tooltipContent = this.createComplexObjectTooltip( + variableName, + singleValue.value, + singleValue.context + ); + } else { + displayText = ` → ${formattedValue}`; + tooltipContent = this.createSingleValueTooltip( + variableName, + formattedValue, + singleValue.context ); + } + } else { + // Multiple values + displayText = " → multiple values"; + tooltipContent = this.createMultipleValuesTooltip( + variableName, + resolvedValues + ); + } - hint.tooltip = new vscode.MarkdownString(tooltipContent); - hint.paddingLeft = true; - hint.paddingRight = false; + const hint = new vscode.InlayHint( + position, + displayText, + vscode.InlayHintKind.Parameter + ); + + hint.tooltip = new vscode.MarkdownString(tooltipContent); + hint.paddingLeft = true; + hint.paddingRight = false; + + return hint; + } + + // NEW: Check if value is a complex object + private isComplexObject(value: string): boolean { + if (!value) return false; + + const trimmed = value.trim(); + + // Check for object notation + if (trimmed.startsWith("{") && trimmed.endsWith("}")) { + const content = trimmed.slice(1, -1).trim(); + // Consider it complex if it has multiple key-value pairs or nested structures + return ( + content.length > 50 || + content.includes("\n") || + content.split("=").length > 2 || + content.includes("{") || + content.includes("[") + ); + } - return hint; + // Check for array notation + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + const content = trimmed.slice(1, -1).trim(); + return ( + content.length > 50 || + content.includes("\n") || + content.includes("{") || + content.split(",").length > 3 + ); } - // NEW: Check if value is a complex object - private isComplexObject(value: string): boolean { - if (!value) return false; - - const trimmed = value.trim(); - - // Check for object notation - if (trimmed.startsWith('{') && trimmed.endsWith('}')) { - const content = trimmed.slice(1, -1).trim(); - // Consider it complex if it has multiple key-value pairs or nested structures - return content.length > 50 || - content.includes('\n') || - content.split('=').length > 2 || - content.includes('{') || - content.includes('['); + return false; + } + + // NEW: Create tooltip for complex objects + private createComplexObjectTooltip( + variableName: string, + value: string, + context: string + ): string { + return ( + `**Terraform Variable:** \`${variableName}\`\n\n` + + `**Context:** \`${context}\`\n\n` + + `**Complex Object Value:**\n\n` + + "```json\n" + + this.formatComplexObjectForTooltip(value) + + "\n```\n\n" + + `*Click to copy value to clipboard*` + ); + } + + // NEW: Create tooltip for single values + private createSingleValueTooltip( + variableName: string, + value: string, + context: string + ): string { + return ( + `**Terraform Variable:** \`${variableName}\`\n\n` + + `**Context:** \`${context}\`\n\n` + + `**Resolved Value:** \`${value}\`\n\n` + + `*Click to copy value to clipboard*` + ); + } + + // NEW: Create tooltip for multiple values + private createMultipleValuesTooltip( + variableName: string, + resolvedValues: Array<{ value: string; context: string; source: string }> + ): string { + const jsonData = this.createJsonFromResolvedValues(resolvedValues); + + return ( + `**Terraform Variable:** \`${variableName}\`\n\n` + + `**Multiple Values Found (${resolvedValues.length}):**\n\n` + + "```json\n" + + jsonData + + "\n```\n\n" + + `*Click to copy JSON to clipboard*` + ); + } + + // NEW: Create JSON from resolved values + private createJsonFromResolvedValues( + resolvedValues: Array<{ value: string; context: string; source: string }> + ): string { + const jsonObject: any = {}; + + for (const resolved of resolvedValues) { + try { + // Try to parse the value as JSON if it looks like an object/array + let parsedValue = resolved.value; + if ( + (resolved.value.trim().startsWith("{") && + resolved.value.trim().endsWith("}")) || + (resolved.value.trim().startsWith("[") && + resolved.value.trim().endsWith("]")) + ) { + try { + // Convert HCL-like syntax to JSON + const jsonString = this.convertHclToJson(resolved.value); + parsedValue = JSON.parse(jsonString); + } catch { + // Keep as string if parsing fails + parsedValue = resolved.value; + } } - - // Check for array notation - if (trimmed.startsWith('[') && trimmed.endsWith(']')) { - const content = trimmed.slice(1, -1).trim(); - return content.length > 50 || - content.includes('\n') || - content.includes('{') || - content.split(',').length > 3; - } - - return false; - } - // NEW: Create tooltip for complex objects - private createComplexObjectTooltip(variableName: string, value: string, context: string): string { - return `**Terraform Variable:** \`${variableName}\`\n\n` + - `**Context:** \`${context}\`\n\n` + - `**Complex Object Value:**\n\n` + - '```json\n' + - this.formatComplexObjectForTooltip(value) + - '\n```\n\n' + - `*Click to copy value to clipboard*`; + jsonObject[resolved.context] = parsedValue; + } catch (error) { + jsonObject[resolved.context] = resolved.value; + } } - // NEW: Create tooltip for single values - private createSingleValueTooltip(variableName: string, value: string, context: string): string { - return `**Terraform Variable:** \`${variableName}\`\n\n` + - `**Context:** \`${context}\`\n\n` + - `**Resolved Value:** \`${value}\`\n\n` + - `*Click to copy value to clipboard*`; + return JSON.stringify(jsonObject, null, 2); + } + + // NEW: Format complex object for tooltip display + private formatComplexObjectForTooltip(value: string): string { + try { + // Try to convert HCL to JSON for better display + const jsonString = this.convertHclToJson(value); + const parsed = JSON.parse(jsonString); + return JSON.stringify(parsed, null, 2); + } catch { + // If conversion fails, return formatted HCL + return value + .split("\n") + .map((line) => " " + line.trim()) + .join("\n"); } + } - // NEW: Create tooltip for multiple values - private createMultipleValuesTooltip( - variableName: string, - resolvedValues: Array<{value: string, context: string, source: string}> - ): string { - const jsonData = this.createJsonFromResolvedValues(resolvedValues); - - return `**Terraform Variable:** \`${variableName}\`\n\n` + - `**Multiple Values Found (${resolvedValues.length}):**\n\n` + - '```json\n' + - jsonData + - '\n```\n\n' + - `*Click to copy JSON to clipboard*`; - } + // NEW: Convert HCL-like syntax to JSON + private convertHclToJson(hclValue: string): string { + if (!hclValue) return "{}"; - // NEW: Create JSON from resolved values - private createJsonFromResolvedValues( - resolvedValues: Array<{value: string, context: string, source: string}> - ): string { - const jsonObject: any = {}; - - for (const resolved of resolvedValues) { - try { - // Try to parse the value as JSON if it looks like an object/array - let parsedValue = resolved.value; - if ((resolved.value.trim().startsWith('{') && resolved.value.trim().endsWith('}')) || - (resolved.value.trim().startsWith('[') && resolved.value.trim().endsWith(']'))) { - try { - // Convert HCL-like syntax to JSON - const jsonString = this.convertHclToJson(resolved.value); - parsedValue = JSON.parse(jsonString); - } catch { - // Keep as string if parsing fails - parsedValue = resolved.value; - } - } - - jsonObject[resolved.context] = parsedValue; - } catch (error) { - jsonObject[resolved.context] = resolved.value; - } - } - - return JSON.stringify(jsonObject, null, 2); + let jsonString = hclValue.trim(); + + // Convert HCL object syntax to JSON + if (jsonString.startsWith("{") && jsonString.endsWith("}")) { + jsonString = jsonString + .replace(/(\w+)\s*=/g, '"$1":') // Convert key = value to "key": value + .replace(/:\s*"([^"]*)"(\s*[,}])/g, ': "$1"$2') // Ensure strings are quoted + .replace(/:\s*([^",}\s]+)(\s*[,}])/g, ': "$1"$2') // Quote unquoted values + .replace(/,(\s*})/g, "$1"); // Remove trailing commas } - // NEW: Format complex object for tooltip display - private formatComplexObjectForTooltip(value: string): string { - try { - // Try to convert HCL to JSON for better display - const jsonString = this.convertHclToJson(value); - const parsed = JSON.parse(jsonString); - return JSON.stringify(parsed, null, 2); - } catch { - // If conversion fails, return formatted HCL - return value.split('\n').map(line => ' ' + line.trim()).join('\n'); + return jsonString; + } + + private isInStringOrComment( + document: vscode.TextDocument, + position: vscode.Position + ): boolean { + try { + const line = document.lineAt(position.line); + const lineText = line.text; + const charIndex = position.character; + + // Check if we're in a comment + const commentIndex = lineText.indexOf("#"); + if (commentIndex !== -1 && charIndex > commentIndex) { + return true; + } + + // Check if we're inside a string + let inString = false; + let stringChar = ""; + let escaped = false; + + for (let i = 0; i < Math.min(charIndex, lineText.length); i++) { + const char = lineText[i]; + + if (escaped) { + escaped = false; + continue; } - } - // NEW: Convert HCL-like syntax to JSON - private convertHclToJson(hclValue: string): string { - if (!hclValue) return '{}'; - - let jsonString = hclValue.trim(); - - // Convert HCL object syntax to JSON - if (jsonString.startsWith('{') && jsonString.endsWith('}')) { - jsonString = jsonString - .replace(/(\w+)\s*=/g, '"$1":') // Convert key = value to "key": value - .replace(/:\s*"([^"]*)"(\s*[,}])/g, ': "$1"$2') // Ensure strings are quoted - .replace(/:\s*([^",}\s]+)(\s*[,}])/g, ': "$1"$2') // Quote unquoted values - .replace(/,(\s*})/g, '$1'); // Remove trailing commas + if (char === "\\") { + escaped = true; + continue; } - - return jsonString; - } - private isInStringOrComment(document: vscode.TextDocument, position: vscode.Position): boolean { - try { - const line = document.lineAt(position.line); - const lineText = line.text; - const charIndex = position.character; - - // Check if we're in a comment - const commentIndex = lineText.indexOf('#'); - if (commentIndex !== -1 && charIndex > commentIndex) { - return true; - } - - // Check if we're inside a string - let inString = false; - let stringChar = ''; - let escaped = false; - - for (let i = 0; i < Math.min(charIndex, lineText.length); i++) { - const char = lineText[i]; - - if (escaped) { - escaped = false; - continue; - } - - if (char === '\\') { - escaped = true; - continue; - } - - if ((char === '"' || char === "'") && !inString) { - inString = true; - stringChar = char; - } else if (char === stringChar && inString) { - inString = false; - stringChar = ''; - } - } - - return inString; - } catch (error) { - this.logger.error('Error checking if position is in string or comment', error); - return false; + if ((char === '"' || char === "'") && !inString) { + inString = true; + stringChar = char; + } else if (char === stringChar && inString) { + inString = false; + stringChar = ""; } + } + + return inString; + } catch (error) { + this.logger.error( + "Error checking if position is in string or comment", + error + ); + return false; } -} \ No newline at end of file + } +} diff --git a/src/resolvers/variableResolver.ts b/src/resolvers/variableResolver.ts index 6ce66fe..0b80c88 100644 --- a/src/resolvers/variableResolver.ts +++ b/src/resolvers/variableResolver.ts @@ -1,10 +1,11 @@ -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import { promisify } from 'util'; -import { Logger } from '../utils/logger'; -import { TerraformParser } from '../parsers/terraformParser'; -import { TerraformCache } from '../utils/cache'; +import * as fs from "fs"; +import * as path from "path"; +import { promisify } from "util"; +import * as vscode from "vscode"; + +import { TerraformParser } from "../parsers/terraformParser"; +import { TerraformCache } from "../utils/cache"; +import { Logger } from "../utils/logger"; const readFile = promisify(fs.readFile); const readdir = promisify(fs.readdir); @@ -12,862 +13,1032 @@ const stat = promisify(fs.stat); const access = promisify(fs.access); interface VariableDefinition { - name: string; - value: any; - type: 'variable' | 'local' | 'output' | 'module_output'; - source: string; - line?: number; + name: string; + value: any; + type: "variable" | "local" | "output" | "module_output"; + source: string; + line?: number; } interface ModuleCall { - name: string; - source: string; - variables: { [key: string]: string }; - location: vscode.Range; - resolvedPath?: string; + name: string; + source: string; + variables: { [key: string]: string }; + location: vscode.Range; + resolvedPath?: string; } export class TerraformVariableResolver { - private cache: TerraformCache; - private parser: TerraformParser; - private fileWatcher: vscode.FileSystemWatcher | null = null; - private readonly maxRecursionDepth = 10; - private resolvedModulePaths = new Map(); - - constructor( - private workspaceRoot: string, - private logger: Logger - ) { - this.cache = new TerraformCache(this.logger); - this.parser = new TerraformParser(this.logger); - this.setupFileWatcher(); - this.logger.info(`TerraformVariableResolver initialized for: ${workspaceRoot}`); + private cache: TerraformCache; + private parser: TerraformParser; + private fileWatcher: vscode.FileSystemWatcher | null = null; + private readonly maxRecursionDepth = 10; + private resolvedModulePaths = new Map(); + + constructor( + private workspaceRoot: string, + private logger: Logger + ) { + this.cache = new TerraformCache(this.logger); + this.parser = new TerraformParser(this.logger); + this.setupFileWatcher(); + this.logger.info( + `TerraformVariableResolver initialized for: ${workspaceRoot}` + ); + } + + async dispose(): Promise { + this.logger.info("Disposing TerraformVariableResolver..."); + + try { + if (this.fileWatcher) { + this.fileWatcher.dispose(); + this.fileWatcher = null; + } + + this.cache.dispose(); + this.resolvedModulePaths.clear(); + } catch (error) { + this.logger.error("Error disposing variable resolver", error); } - - async dispose(): Promise { - this.logger.info('Disposing TerraformVariableResolver...'); - - try { - if (this.fileWatcher) { - this.fileWatcher.dispose(); - this.fileWatcher = null; - } - - this.cache.dispose(); - this.resolvedModulePaths.clear(); - } catch (error) { - this.logger.error('Error disposing variable resolver', error); - } + } + + async clearCache(): Promise { + this.cache.clear(); + this.resolvedModulePaths.clear(); + this.logger.info("Variable resolver cache cleared"); + } + + getCacheSize(): number { + return this.cache.size(); + } + + getFromCache(key: string): string | null { + return this.cache.get(key); + } + + setCache(key: string, value: string): void { + this.cache.set(key, value); + } + + private setupFileWatcher(): void { + try { + this.fileWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern( + this.workspaceRoot, + "**/*.{tf,tfvars,tfvars.json}" + ) + ); + + this.fileWatcher.onDidChange((uri) => { + this.handleFileChange(uri.fsPath, "changed"); + }); + + this.fileWatcher.onDidCreate((uri) => { + this.handleFileChange(uri.fsPath, "created"); + }); + + this.fileWatcher.onDidDelete((uri) => { + this.handleFileChange(uri.fsPath, "deleted"); + }); + + this.logger.debug("File watcher setup completed"); + } catch (error) { + this.logger.error("Failed to setup file watcher", error); } - - async clearCache(): Promise { - this.cache.clear(); - this.resolvedModulePaths.clear(); - this.logger.info('Variable resolver cache cleared'); + } + + private handleFileChange(filePath: string, changeType: string): void { + try { + this.cache.invalidateFile(filePath); + this.logger.debug(`File ${changeType}: ${filePath}, cache invalidated`); + } catch (error) { + this.logger.error(`Error handling file change: ${filePath}`, error); } - - getCacheSize(): number { - return this.cache.size(); + } + + async resolveVariableValue( + variableName: string, + currentDir: string, + visited: Set = new Set(), + depth: number = 0 + ): Promise { + if (depth > this.maxRecursionDepth) { + this.logger.warn( + `Maximum recursion depth reached for variable: ${variableName}` + ); + return null; } - getFromCache(key: string): string | null { - return this.cache.get(key); + const cacheKey = `resolve:${variableName}:${currentDir}:${depth}`; + const cached = this.cache.get(cacheKey); + if (cached !== null) { + return cached; } - setCache(key: string, value: string): void { - this.cache.set(key, value); + // Prevent infinite recursion + const visitKey = `${currentDir}:${variableName}:${depth}`; + if (visited.has(visitKey)) { + this.logger.debug( + `Circular reference detected for: ${variableName} in ${currentDir}` + ); + return null; } - - private setupFileWatcher(): void { - try { - this.fileWatcher = vscode.workspace.createFileSystemWatcher( - new vscode.RelativePattern(this.workspaceRoot, '**/*.{tf,tfvars,tfvars.json}') - ); - - this.fileWatcher.onDidChange((uri) => { - this.handleFileChange(uri.fsPath, 'changed'); - }); - - this.fileWatcher.onDidCreate((uri) => { - this.handleFileChange(uri.fsPath, 'created'); - }); - - this.fileWatcher.onDidDelete((uri) => { - this.handleFileChange(uri.fsPath, 'deleted'); - }); - - this.logger.debug('File watcher setup completed'); - } catch (error) { - this.logger.error('Failed to setup file watcher', error); + visited.add(visitKey); + + try { + let resolvedValue: string | null = null; + + // Resolution order: tfvars -> locals -> outputs -> module outputs -> parent directories + resolvedValue = await this.findInTfvarsFiles(variableName, currentDir); + if (resolvedValue !== null) { + this.cache.set(cacheKey, resolvedValue); + return resolvedValue; + } + + resolvedValue = await this.findInLocals(variableName, currentDir); + if (resolvedValue !== null) { + this.cache.set(cacheKey, resolvedValue); + return resolvedValue; + } + + resolvedValue = await this.findInOutputs(variableName, currentDir); + if (resolvedValue !== null) { + this.cache.set(cacheKey, resolvedValue); + return resolvedValue; + } + + // Handle module output references + if (variableName.startsWith("module.")) { + resolvedValue = await this.resolveModuleOutput( + variableName, + currentDir, + visited, + depth + 1 + ); + if (resolvedValue !== null) { + this.cache.set(cacheKey, resolvedValue); + return resolvedValue; + } + } + + // Search in parent directory + const parentDir = path.dirname(currentDir); + if (parentDir !== currentDir && this.isWithinWorkspace(parentDir)) { + resolvedValue = await this.resolveVariableValue( + variableName, + parentDir, + visited, + depth + 1 + ); + if (resolvedValue !== null) { + this.cache.set(cacheKey, resolvedValue); + return resolvedValue; } + } + + return null; + } catch (error) { + this.logger.error( + `Error resolving variable ${variableName} in ${currentDir}`, + error + ); + return null; + } finally { + visited.delete(visitKey); } - - private handleFileChange(filePath: string, changeType: string): void { - try { - this.cache.invalidateFile(filePath); - this.logger.debug(`File ${changeType}: ${filePath}, cache invalidated`); - } catch (error) { - this.logger.error(`Error handling file change: ${filePath}`, error); + } + + private isWithinWorkspace(dirPath: string): boolean { + const normalizedDir = path.normalize(dirPath); + const normalizedWorkspace = path.normalize(this.workspaceRoot); + return normalizedDir.startsWith(normalizedWorkspace); + } + + async resolveVariableInMultipleContexts( + variableName: string, + searchDirectories: string[] + ): Promise> { + const results: Array<{ value: string; directory: string }> = []; + const promises = searchDirectories.map(async (dir) => { + try { + const value = await this.resolveVariableValue(variableName, dir); + if (value && value.trim() !== "") { + return { value, directory: dir }; } + } catch (error) { + this.logger.debug(`Failed to resolve ${variableName} in ${dir}`, error); + } + return null; + }); + + const resolvedResults = await Promise.all(promises); + + for (const result of resolvedResults) { + if (result) { + results.push(result); + } } - async resolveVariableValue( - variableName: string, - currentDir: string, - visited: Set = new Set(), - depth: number = 0 - ): Promise { - if (depth > this.maxRecursionDepth) { - this.logger.warn(`Maximum recursion depth reached for variable: ${variableName}`); - return null; - } + return results; + } - const cacheKey = `resolve:${variableName}:${currentDir}:${depth}`; - const cached = this.cache.get(cacheKey); - if (cached !== null) { - return cached; - } + // MODIFIED: Enhanced findInTfvarsFiles to handle complex objects better + private async findInTfvarsFiles( + variableName: string, + dir: string + ): Promise { + try { + const cacheKey = `tfvars:${variableName}:${dir}`; + const cached = this.cache.get(cacheKey); + if (cached !== null) return cached; - // Prevent infinite recursion - const visitKey = `${currentDir}:${variableName}:${depth}`; - if (visited.has(visitKey)) { - this.logger.debug(`Circular reference detected for: ${variableName} in ${currentDir}`); - return null; - } - visited.add(visitKey); + if (!(await this.directoryExists(dir))) { + return null; + } + + const files = await readdir(dir); + const tfvarsFiles = files.filter( + (f) => f.endsWith(".tfvars") || f.endsWith(".tfvars.json") + ); + + for (const file of tfvarsFiles) { + const filePath = path.join(dir, file); try { - let resolvedValue: string | null = null; - - // Resolution order: tfvars -> locals -> outputs -> module outputs -> parent directories - resolvedValue = await this.findInTfvarsFiles(variableName, currentDir); - if (resolvedValue !== null) { - this.cache.set(cacheKey, resolvedValue); - return resolvedValue; - } - - resolvedValue = await this.findInLocals(variableName, currentDir); - if (resolvedValue !== null) { - this.cache.set(cacheKey, resolvedValue); - return resolvedValue; - } - - resolvedValue = await this.findInOutputs(variableName, currentDir); - if (resolvedValue !== null) { - this.cache.set(cacheKey, resolvedValue); - return resolvedValue; - } - - // Handle module output references - if (variableName.startsWith('module.')) { - resolvedValue = await this.resolveModuleOutput(variableName, currentDir, visited, depth + 1); - if (resolvedValue !== null) { - this.cache.set(cacheKey, resolvedValue); - return resolvedValue; - } - } - - // Search in parent directory - const parentDir = path.dirname(currentDir); - if (parentDir !== currentDir && this.isWithinWorkspace(parentDir)) { - resolvedValue = await this.resolveVariableValue(variableName, parentDir, visited, depth + 1); - if (resolvedValue !== null) { - this.cache.set(cacheKey, resolvedValue); - return resolvedValue; - } - } - - return null; + const content = await readFile(filePath, "utf8"); + let value: string | null = null; + + if (file.endsWith(".json")) { + value = this.parser.parseJsonVariable(content, variableName); + } else { + value = this.parser.parseHclVariable(content, variableName); + } + + if (value !== null) { + // Enhanced: Preserve complex objects in their original form + const preservedValue = this.preserveComplexStructure(value); + this.cache.set(cacheKey, preservedValue); + this.logger.debug(`Found variable ${variableName} in ${filePath}`); + return preservedValue; + } } catch (error) { - this.logger.error(`Error resolving variable ${variableName} in ${currentDir}`, error); - return null; - } finally { - visited.delete(visitKey); + this.logger.error(`Error reading tfvars file: ${filePath}`, error); + continue; } - } + } - private isWithinWorkspace(dirPath: string): boolean { - const normalizedDir = path.normalize(dirPath); - const normalizedWorkspace = path.normalize(this.workspaceRoot); - return normalizedDir.startsWith(normalizedWorkspace); + return null; + } catch (error) { + this.logger.error(`Error searching tfvars files in ${dir}`, error); + return null; } - - async resolveVariableInMultipleContexts( - variableName: string, - searchDirectories: string[] - ): Promise> { - const results: Array<{value: string, directory: string}> = []; - const promises = searchDirectories.map(async (dir) => { - try { - const value = await this.resolveVariableValue(variableName, dir); - if (value && value.trim() !== '') { - return { value, directory: dir }; - } - } catch (error) { - this.logger.debug(`Failed to resolve ${variableName} in ${dir}`, error); - } - return null; - }); - - const resolvedResults = await Promise.all(promises); - - for (const result of resolvedResults) { - if (result) { - results.push(result); - } - } - - return results; + } + + // NEW: Enhanced recursive resolution that follows module variable chains + async resolveVariableValueEnhanced( + variableName: string, + currentDir: string, + visited: Set = new Set(), + depth: number = 0, + moduleContext: string[] = [] + ): Promise { + if (depth > this.maxRecursionDepth) { + this.logger.warn( + `Maximum recursion depth reached for variable: ${variableName}` + ); + return null; } - // MODIFIED: Enhanced findInTfvarsFiles to handle complex objects better - private async findInTfvarsFiles(variableName: string, dir: string): Promise { - try { - const cacheKey = `tfvars:${variableName}:${dir}`; - const cached = this.cache.get(cacheKey); - if (cached !== null) return cached; - - if (!(await this.directoryExists(dir))) { - return null; - } - - const files = await readdir(dir); - const tfvarsFiles = files.filter(f => - f.endsWith('.tfvars') || f.endsWith('.tfvars.json') - ); - - for (const file of tfvarsFiles) { - const filePath = path.join(dir, file); - - try { - const content = await readFile(filePath, 'utf8'); - let value: string | null = null; - - if (file.endsWith('.json')) { - value = this.parser.parseJsonVariable(content, variableName); - } else { - value = this.parser.parseHclVariable(content, variableName); - } - - if (value !== null) { - // Enhanced: Preserve complex objects in their original form - const preservedValue = this.preserveComplexStructure(value); - this.cache.set(cacheKey, preservedValue); - this.logger.debug(`Found variable ${variableName} in ${filePath}`); - return preservedValue; - } - } catch (error) { - this.logger.error(`Error reading tfvars file: ${filePath}`, error); - continue; - } - } - - return null; - } catch (error) { - this.logger.error(`Error searching tfvars files in ${dir}`, error); - return null; - } + const cacheKey = `enhanced:${variableName}:${currentDir}:${depth}:${moduleContext.join(",")}`; + const cached = this.cache.get(cacheKey); + if (cached !== null) { + return cached; } - // NEW: Enhanced recursive resolution that follows module variable chains - async resolveVariableValueEnhanced( - variableName: string, - currentDir: string, - visited: Set = new Set(), - depth: number = 0, - moduleContext: string[] = [] - ): Promise { - if (depth > this.maxRecursionDepth) { - this.logger.warn(`Maximum recursion depth reached for variable: ${variableName}`); - return null; + // Prevent infinite recursion + const visitKey = `${currentDir}:${variableName}:${depth}`; + if (visited.has(visitKey)) { + this.logger.debug( + `Circular reference detected for: ${variableName} in ${currentDir}` + ); + return null; + } + visited.add(visitKey); + + try { + let resolvedValue: string | null = null; + + // FIXED: Enhanced resolution order with module context awareness + + // 1. First, try direct resolution in current directory + resolvedValue = await this.findInTfvarsFiles(variableName, currentDir); + if (resolvedValue !== null) { + const finalValue = await this.resolveNestedReferences( + resolvedValue, + currentDir, + visited, + depth + 1 + ); + this.cache.set(cacheKey, finalValue); + return finalValue; + } + + resolvedValue = await this.findInLocals(variableName, currentDir); + if (resolvedValue !== null) { + const finalValue = await this.resolveNestedReferences( + resolvedValue, + currentDir, + visited, + depth + 1 + ); + this.cache.set(cacheKey, finalValue); + return finalValue; + } + + resolvedValue = await this.findInOutputs(variableName, currentDir); + if (resolvedValue !== null) { + const finalValue = await this.resolveNestedReferences( + resolvedValue, + currentDir, + visited, + depth + 1 + ); + this.cache.set(cacheKey, finalValue); + return finalValue; + } + + // 2. FIXED: Check if we're in a module and the variable might be passed from parent + if (moduleContext.length === 0) { + const moduleVariable = await this.resolveAsModuleInput( + variableName, + currentDir, + visited, + depth + 1 + ); + if (moduleVariable !== null) { + this.cache.set(cacheKey, moduleVariable); + return moduleVariable; } - - const cacheKey = `enhanced:${variableName}:${currentDir}:${depth}:${moduleContext.join(',')}`; - const cached = this.cache.get(cacheKey); - if (cached !== null) { - return cached; + } + + // 3. Handle module output references + if (variableName.startsWith("module.")) { + resolvedValue = await this.resolveModuleOutput( + variableName, + currentDir, + visited, + depth + 1 + ); + if (resolvedValue !== null) { + const finalValue = await this.resolveNestedReferences( + resolvedValue, + currentDir, + visited, + depth + 1 + ); + this.cache.set(cacheKey, finalValue); + return finalValue; } - - // Prevent infinite recursion - const visitKey = `${currentDir}:${variableName}:${depth}`; - if (visited.has(visitKey)) { - this.logger.debug(`Circular reference detected for: ${variableName} in ${currentDir}`); - return null; + } + + // 4. FIXED: Search in parent directories with enhanced logic + const parentDir = path.dirname(currentDir); + if (parentDir !== currentDir && this.isWithinWorkspace(parentDir)) { + // Check if parent has a module that might be calling our current directory + const parentModuleValue = await this.findVariableInParentModule( + variableName, + currentDir, + parentDir, + visited, + depth + 1 + ); + if (parentModuleValue !== null) { + this.cache.set(cacheKey, parentModuleValue); + return parentModuleValue; } - visited.add(visitKey); - try { - let resolvedValue: string | null = null; - - // FIXED: Enhanced resolution order with module context awareness - - // 1. First, try direct resolution in current directory - resolvedValue = await this.findInTfvarsFiles(variableName, currentDir); - if (resolvedValue !== null) { - const finalValue = await this.resolveNestedReferences(resolvedValue, currentDir, visited, depth + 1); - this.cache.set(cacheKey, finalValue); - return finalValue; - } - - resolvedValue = await this.findInLocals(variableName, currentDir); - if (resolvedValue !== null) { - const finalValue = await this.resolveNestedReferences(resolvedValue, currentDir, visited, depth + 1); - this.cache.set(cacheKey, finalValue); - return finalValue; - } - - resolvedValue = await this.findInOutputs(variableName, currentDir); - if (resolvedValue !== null) { - const finalValue = await this.resolveNestedReferences(resolvedValue, currentDir, visited, depth + 1); - this.cache.set(cacheKey, finalValue); - return finalValue; - } - - // 2. FIXED: Check if we're in a module and the variable might be passed from parent - if (moduleContext.length === 0) { - const moduleVariable = await this.resolveAsModuleInput(variableName, currentDir, visited, depth + 1); - if (moduleVariable !== null) { - this.cache.set(cacheKey, moduleVariable); - return moduleVariable; - } - } - - // 3. Handle module output references - if (variableName.startsWith('module.')) { - resolvedValue = await this.resolveModuleOutput(variableName, currentDir, visited, depth + 1); - if (resolvedValue !== null) { - const finalValue = await this.resolveNestedReferences(resolvedValue, currentDir, visited, depth + 1); - this.cache.set(cacheKey, finalValue); - return finalValue; - } - } - - // 4. FIXED: Search in parent directories with enhanced logic - const parentDir = path.dirname(currentDir); - if (parentDir !== currentDir && this.isWithinWorkspace(parentDir)) { - // Check if parent has a module that might be calling our current directory - const parentModuleValue = await this.findVariableInParentModule(variableName, currentDir, parentDir, visited, depth + 1); - if (parentModuleValue !== null) { - this.cache.set(cacheKey, parentModuleValue); - return parentModuleValue; - } - - // Regular parent directory search - resolvedValue = await this.resolveVariableValueEnhanced(variableName, parentDir, visited, depth + 1, moduleContext); - if (resolvedValue !== null) { - this.cache.set(cacheKey, resolvedValue); - return resolvedValue; - } - } - - return null; - } catch (error) { - this.logger.error(`Error in enhanced resolution for ${variableName} in ${currentDir}`, error); - return null; - } finally { - visited.delete(visitKey); + // Regular parent directory search + resolvedValue = await this.resolveVariableValueEnhanced( + variableName, + parentDir, + visited, + depth + 1, + moduleContext + ); + if (resolvedValue !== null) { + this.cache.set(cacheKey, resolvedValue); + return resolvedValue; } + } + + return null; + } catch (error) { + this.logger.error( + `Error in enhanced resolution for ${variableName} in ${currentDir}`, + error + ); + return null; + } finally { + visited.delete(visitKey); } - - // NEW: Resolve variable as module input from parent - private async resolveAsModuleInput( - variableName: string, - moduleDir: string, - visited: Set, - depth: number - ): Promise { - try { - const parentDir = path.dirname(moduleDir); - if (!this.isWithinWorkspace(parentDir)) { - return null; - } - - // Find modules in parent that reference our current directory - const moduleCalls = await this.findModuleCallsToDirectory(parentDir, moduleDir); - - for (const moduleCall of moduleCalls) { - // Check if this module call passes our variable - if (moduleCall.variables[variableName]) { - const variableReference = moduleCall.variables[variableName]; - - // Resolve the variable reference in the parent context - const resolvedRef = await this.resolveVariableReference(variableReference, parentDir, visited, depth); - if (resolvedRef !== null) { - this.logger.debug(`Resolved ${variableName} via module input: ${variableReference} -> ${resolvedRef}`); - return resolvedRef; - } - } - } - - return null; - } catch (error) { - this.logger.error(`Error resolving module input for ${variableName}`, error); - return null; + } + + // NEW: Resolve variable as module input from parent + private async resolveAsModuleInput( + variableName: string, + moduleDir: string, + visited: Set, + depth: number + ): Promise { + try { + const parentDir = path.dirname(moduleDir); + if (!this.isWithinWorkspace(parentDir)) { + return null; + } + + // Find modules in parent that reference our current directory + const moduleCalls = await this.findModuleCallsToDirectory( + parentDir, + moduleDir + ); + + for (const moduleCall of moduleCalls) { + // Check if this module call passes our variable + if (moduleCall.variables[variableName]) { + const variableReference = moduleCall.variables[variableName]; + + // Resolve the variable reference in the parent context + const resolvedRef = await this.resolveVariableReference( + variableReference, + parentDir, + visited, + depth + ); + if (resolvedRef !== null) { + this.logger.debug( + `Resolved ${variableName} via module input: ${variableReference} -> ${resolvedRef}` + ); + return resolvedRef; + } } + } + + return null; + } catch (error) { + this.logger.error( + `Error resolving module input for ${variableName}`, + error + ); + return null; } - - // NEW: Find module calls that point to a specific directory - private async findModuleCallsToDirectory(searchDir: string, targetDir: string): Promise> { - const moduleCalls: Array<{name: string; source: string; variables: { [key: string]: string }}> = []; - - try { - if (!(await this.directoryExists(searchDir))) { - return moduleCalls; - } - - const files = await readdir(searchDir); - const tfFiles = files.filter(f => f.endsWith('.tf')); - - for (const file of tfFiles) { - const filePath = path.join(searchDir, file); - const content = await readFile(filePath, 'utf8'); - - // Find all module blocks - const moduleRegex = /module\s+"(\w+)"\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs; - let match; - - while ((match = moduleRegex.exec(content)) !== null) { - const moduleName = match[1]; - const moduleContent = match[2]; - - // Extract source - const sourceMatch = /source\s*=\s*"([^"]*)"/.exec(moduleContent); - if (!sourceMatch) continue; - - // Check if source points to our target directory - const resolvedSource = await this.resolveModulePath(sourceMatch[1], searchDir); - if (resolvedSource === targetDir) { - moduleCalls.push({ - name: moduleName, - source: sourceMatch[1], - variables: this.parser.parseModuleVariables(moduleContent) - }); - } - } - } - } catch (error) { - this.logger.error(`Error finding module calls to ${targetDir}`, error); - } - + } + + // NEW: Find module calls that point to a specific directory + private async findModuleCallsToDirectory( + searchDir: string, + targetDir: string + ): Promise< + Array<{ + name: string; + source: string; + variables: { [key: string]: string }; + }> + > { + const moduleCalls: Array<{ + name: string; + source: string; + variables: { [key: string]: string }; + }> = []; + + try { + if (!(await this.directoryExists(searchDir))) { return moduleCalls; - } - - // NEW: Resolve a variable reference (like var.something) in a specific context - private async resolveVariableReference( - reference: string, - contextDir: string, - visited: Set, - depth: number - ): Promise { - try { - // Clean the reference (remove quotes, etc.) - const cleanRef = reference.replace(/['"]/g, '').trim(); - - // Handle different reference types - if (cleanRef.startsWith('var.')) { - const varName = cleanRef.substring(4); - return await this.resolveVariableValueEnhanced(varName, contextDir, visited, depth); - } else if (cleanRef.startsWith('local.')) { - const localName = cleanRef.substring(6); - return await this.findInLocals(localName, contextDir); - } else if (cleanRef.startsWith('module.')) { - return await this.resolveModuleOutput(cleanRef, contextDir, visited, depth); - } else { - // Direct value - return cleanRef; - } - } catch (error) { - this.logger.error(`Error resolving variable reference: ${reference}`, error); - return null; + } + + const files = await readdir(searchDir); + const tfFiles = files.filter((f) => f.endsWith(".tf")); + + for (const file of tfFiles) { + const filePath = path.join(searchDir, file); + const content = await readFile(filePath, "utf8"); + + // Find all module blocks + const moduleRegex = + /module\s+"(\w+)"\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs; + let match; + + while ((match = moduleRegex.exec(content)) !== null) { + const moduleName = match[1]; + const moduleContent = match[2]; + + // Extract source + const sourceMatch = /source\s*=\s*"([^"]*)"/.exec(moduleContent); + if (!sourceMatch) continue; + + // Check if source points to our target directory + const resolvedSource = await this.resolveModulePath( + sourceMatch[1], + searchDir + ); + if (resolvedSource === targetDir) { + moduleCalls.push({ + name: moduleName, + source: sourceMatch[1], + variables: this.parser.parseModuleVariables(moduleContent), + }); + } } + } + } catch (error) { + this.logger.error(`Error finding module calls to ${targetDir}`, error); } - // NEW: Find variable in parent module that calls current directory - private async findVariableInParentModule( - variableName: string, - currentDir: string, - parentDir: string, - visited: Set, - depth: number - ): Promise { - try { - // Find modules in parent that call our current directory - const moduleCalls = await this.findModuleCallsToDirectory(parentDir, currentDir); - - for (const moduleCall of moduleCalls) { - // Look for variable assignments in the module call - if (moduleCall.variables[variableName]) { - const variableRef = moduleCall.variables[variableName]; - const resolved = await this.resolveVariableReference(variableRef, parentDir, visited, depth); - if (resolved !== null) { - return resolved; - } - } - } - - return null; - } catch (error) { - this.logger.error(`Error finding variable in parent module`, error); - return null; + return moduleCalls; + } + + // NEW: Resolve a variable reference (like var.something) in a specific context + private async resolveVariableReference( + reference: string, + contextDir: string, + visited: Set, + depth: number + ): Promise { + try { + // Clean the reference (remove quotes, etc.) + const cleanRef = reference.replace(/['"]/g, "").trim(); + + // Handle different reference types + if (cleanRef.startsWith("var.")) { + const varName = cleanRef.substring(4); + return await this.resolveVariableValueEnhanced( + varName, + contextDir, + visited, + depth + ); + } else if (cleanRef.startsWith("local.")) { + const localName = cleanRef.substring(6); + return await this.findInLocals(localName, contextDir); + } else if (cleanRef.startsWith("module.")) { + return await this.resolveModuleOutput( + cleanRef, + contextDir, + visited, + depth + ); + } else { + // Direct value + return cleanRef; + } + } catch (error) { + this.logger.error( + `Error resolving variable reference: ${reference}`, + error + ); + return null; + } + } + + // NEW: Find variable in parent module that calls current directory + private async findVariableInParentModule( + variableName: string, + currentDir: string, + parentDir: string, + visited: Set, + depth: number + ): Promise { + try { + // Find modules in parent that call our current directory + const moduleCalls = await this.findModuleCallsToDirectory( + parentDir, + currentDir + ); + + for (const moduleCall of moduleCalls) { + // Look for variable assignments in the module call + if (moduleCall.variables[variableName]) { + const variableRef = moduleCall.variables[variableName]; + const resolved = await this.resolveVariableReference( + variableRef, + parentDir, + visited, + depth + ); + if (resolved !== null) { + return resolved; + } } + } + + return null; + } catch (error) { + this.logger.error(`Error finding variable in parent module`, error); + return null; + } + } + + // NEW: Resolve nested references within a resolved value + private async resolveNestedReferences( + value: string, + contextDir: string, + visited: Set, + depth: number + ): Promise { + if (!value || typeof value !== "string") { + return value; } - // NEW: Resolve nested references within a resolved value - private async resolveNestedReferences( - value: string, - contextDir: string, - visited: Set, - depth: number - ): Promise { - if (!value || typeof value !== 'string') { - return value; + try { + let resolvedValue = value; + + // Find and resolve all variable references in the value + const varRefs = this.parser.extractVariableReferences(value); + + for (const varRef of varRefs) { + const resolved = await this.resolveVariableReference( + varRef, + contextDir, + visited, + depth + ); + if (resolved !== null && resolved !== varRef) { + // Replace the reference with the resolved value + const refPattern = new RegExp( + `\\b${varRef.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, + "g" + ); + resolvedValue = resolvedValue.replace(refPattern, resolved); } + } - try { - let resolvedValue = value; - - // Find and resolve all variable references in the value - const varRefs = this.parser.extractVariableReferences(value); - - for (const varRef of varRefs) { - const resolved = await this.resolveVariableReference(varRef, contextDir, visited, depth); - if (resolved !== null && resolved !== varRef) { - // Replace the reference with the resolved value - const refPattern = new RegExp(`\\b${varRef.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'); - resolvedValue = resolvedValue.replace(refPattern, resolved); - } - } - - return resolvedValue; - } catch (error) { - this.logger.error('Error resolving nested references', error); - return value; - } + return resolvedValue; + } catch (error) { + this.logger.error("Error resolving nested references", error); + return value; } + } + + // NEW: Preserve complex structure formatting + private preserveComplexStructure(value: string): string { + if (!value) return value; - // NEW: Preserve complex structure formatting - private preserveComplexStructure(value: string): string { - if (!value) return value; - - const trimmed = value.trim(); - - // If it's a complex object or array, preserve formatting - if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || - (trimmed.startsWith('[') && trimmed.endsWith(']'))) { - - // Try to format it nicely - try { - // If it's JSON-like, parse and re-stringify with formatting - if (this.looksLikeJson(trimmed)) { - const parsed = JSON.parse(trimmed); - return JSON.stringify(parsed, null, 2); - } - - // If it's HCL-like, preserve the structure but clean it up - return this.formatHclStructure(trimmed); - } catch { - // If formatting fails, return as-is - return trimmed; - } + const trimmed = value.trim(); + + // If it's a complex object or array, preserve formatting + if ( + (trimmed.startsWith("{") && trimmed.endsWith("}")) || + (trimmed.startsWith("[") && trimmed.endsWith("]")) + ) { + // Try to format it nicely + try { + // If it's JSON-like, parse and re-stringify with formatting + if (this.looksLikeJson(trimmed)) { + const parsed = JSON.parse(trimmed); + return JSON.stringify(parsed, null, 2); } - + + // If it's HCL-like, preserve the structure but clean it up + return this.formatHclStructure(trimmed); + } catch { + // If formatting fails, return as-is return trimmed; + } } - // NEW: Check if string looks like JSON - private looksLikeJson(str: string): boolean { - try { - JSON.parse(str); - return true; - } catch { - return false; - } - } + return trimmed; + } - // NEW: Format HCL structure for better display - private formatHclStructure(hclStr: string): string { - if (!hclStr) return hclStr; - - // Basic HCL formatting - let formatted = hclStr; - - // Add proper line breaks after commas in objects - formatted = formatted.replace(/,\s*(?=[^"]*(?:"[^"]*"[^"]*)*$)/g, ',\n '); - - // Add proper indentation - const lines = formatted.split('\n'); - let indentLevel = 0; - const indentedLines = lines.map(line => { - const trimmedLine = line.trim(); - - if (trimmedLine.includes('}') || trimmedLine.includes(']')) { - indentLevel = Math.max(0, indentLevel - 1); - } - - const indentedLine = ' '.repeat(indentLevel) + trimmedLine; - - if (trimmedLine.includes('{') || trimmedLine.includes('[')) { - indentLevel++; - } - - return indentedLine; - }); - - return indentedLines.join('\n'); + // NEW: Check if string looks like JSON + private looksLikeJson(str: string): boolean { + try { + JSON.parse(str); + return true; + } catch { + return false; } + } - private async findInOutputs(variableName: string, dir: string): Promise { - try { - const cacheKey = `outputs:${variableName}:${dir}`; - const cached = this.cache.get(cacheKey); - if (cached !== null) return cached; - - if (!(await this.directoryExists(dir))) { - return null; - } - - const files = await readdir(dir); - const tfFiles = files.filter(f => f.endsWith('.tf')); - - for (const file of tfFiles) { - const filePath = path.join(dir, file); - - try { - const content = await readFile(filePath, 'utf8'); - const value = this.parser.parseOutputBlock(content, variableName); - - if (value !== null) { - this.cache.set(cacheKey, value); - this.logger.debug(`Found output ${variableName} in ${filePath}`); - return value; - } - } catch (error) { - this.logger.error(`Error reading tf file: ${filePath}`, error); - continue; - } - } - - return null; - } catch (error) { - this.logger.error(`Error searching outputs in ${dir}`, error); - return null; - } - } + // NEW: Format HCL structure for better display + private formatHclStructure(hclStr: string): string { + if (!hclStr) return hclStr; - private async resolveModuleOutput( - variableRef: string, - currentDir: string, - visited: Set, - depth: number - ): Promise { - try { - // Parse module.module_name.output_name - const parts = variableRef.split('.'); - if (parts.length < 3 || parts[0] !== 'module') { - return null; - } - - const moduleName = parts[1]; - const outputName = parts.slice(2).join('.'); - - // Find the module definition - const moduleCall = await this.findModuleCall(moduleName, currentDir); - if (!moduleCall) { - this.logger.debug(`Module call not found: ${moduleName} in ${currentDir}`); - return null; - } - - // Resolve module source path - const modulePath = await this.resolveModulePath(moduleCall.source, currentDir); - if (!modulePath) { - this.logger.debug(`Module path could not be resolved: ${moduleCall.source}`); - return null; - } - - // Look for the output in the module - const outputValue = await this.findInOutputs(outputName, modulePath); - if (outputValue !== null) { - // If output references other variables, resolve them recursively - return await this.resolveReferencesInValue(outputValue, modulePath, visited, depth + 1); - } - - return null; - } catch (error) { - this.logger.error(`Error resolving module output: ${variableRef}`, error); - return null; - } - } + // Basic HCL formatting + let formatted = hclStr; - private async resolveReferencesInValue( - value: string, - contextDir: string, - visited: Set, - depth: number - ): Promise { - if (!value || typeof value !== 'string') { - return value; - } + // Add proper line breaks after commas in objects + formatted = formatted.replace(/,\s*(?=[^"]*(?:"[^"]*"[^"]*)*$)/g, ",\n "); + + // Add proper indentation + const lines = formatted.split("\n"); + let indentLevel = 0; + const indentedLines = lines.map((line) => { + const trimmedLine = line.trim(); + + if (trimmedLine.includes("}") || trimmedLine.includes("]")) { + indentLevel = Math.max(0, indentLevel - 1); + } + + const indentedLine = " ".repeat(indentLevel) + trimmedLine; + + if (trimmedLine.includes("{") || trimmedLine.includes("[")) { + indentLevel++; + } + + return indentedLine; + }); + + return indentedLines.join("\n"); + } + + private async findInOutputs( + variableName: string, + dir: string + ): Promise { + try { + const cacheKey = `outputs:${variableName}:${dir}`; + const cached = this.cache.get(cacheKey); + if (cached !== null) return cached; + + if (!(await this.directoryExists(dir))) { + return null; + } + + const files = await readdir(dir); + const tfFiles = files.filter((f) => f.endsWith(".tf")); + + for (const file of tfFiles) { + const filePath = path.join(dir, file); try { - // Find variable references in the value - const varRefs = this.parser.extractVariableReferences(value); - let resolvedValue = value; - - for (const varRef of varRefs) { - const resolvedRef = await this.resolveVariableValue(varRef, contextDir, visited, depth); - if (resolvedRef !== null) { - // Replace the variable reference with resolved value - const refPattern = new RegExp(`\\b${varRef.replace('.', '\\.')}\\b`, 'g'); - resolvedValue = resolvedValue.replace(refPattern, resolvedRef); - } - } - - return resolvedValue; - } catch (error) { - this.logger.error('Error resolving references in value', error); + const content = await readFile(filePath, "utf8"); + const value = this.parser.parseOutputBlock(content, variableName); + + if (value !== null) { + this.cache.set(cacheKey, value); + this.logger.debug(`Found output ${variableName} in ${filePath}`); return value; + } + } catch (error) { + this.logger.error(`Error reading tf file: ${filePath}`, error); + continue; } + } + + return null; + } catch (error) { + this.logger.error(`Error searching outputs in ${dir}`, error); + return null; + } + } + + private async resolveModuleOutput( + variableRef: string, + currentDir: string, + visited: Set, + depth: number + ): Promise { + try { + // Parse module.module_name.output_name + const parts = variableRef.split("."); + if (parts.length < 3 || parts[0] !== "module") { + return null; + } + + const moduleName = parts[1]; + const outputName = parts.slice(2).join("."); + + // Find the module definition + const moduleCall = await this.findModuleCall(moduleName, currentDir); + if (!moduleCall) { + this.logger.debug( + `Module call not found: ${moduleName} in ${currentDir}` + ); + return null; + } + + // Resolve module source path + const modulePath = await this.resolveModulePath( + moduleCall.source, + currentDir + ); + if (!modulePath) { + this.logger.debug( + `Module path could not be resolved: ${moduleCall.source}` + ); + return null; + } + + // Look for the output in the module + const outputValue = await this.findInOutputs(outputName, modulePath); + if (outputValue !== null) { + // If output references other variables, resolve them recursively + return await this.resolveReferencesInValue( + outputValue, + modulePath, + visited, + depth + 1 + ); + } + + return null; + } catch (error) { + this.logger.error(`Error resolving module output: ${variableRef}`, error); + return null; + } + } + + private async resolveReferencesInValue( + value: string, + contextDir: string, + visited: Set, + depth: number + ): Promise { + if (!value || typeof value !== "string") { + return value; } - private async findModuleCall(moduleName: string, dir: string): Promise { - try { - const cacheKey = `module:${moduleName}:${dir}`; - const cached = this.cache.get(cacheKey); - if (cached !== null) { - return JSON.parse(cached); - } - - if (!(await this.directoryExists(dir))) { - return null; - } - - const files = await readdir(dir); - const tfFiles = files.filter(f => f.endsWith('.tf')); - - for (const file of tfFiles) { - const filePath = path.join(dir, file); - - try { - const content = await readFile(filePath, 'utf8'); - const moduleCall = this.parser.parseModuleBlock(content, moduleName); - - if (moduleCall) { - this.cache.set(cacheKey, JSON.stringify(moduleCall)); - this.logger.debug(`Found module call ${moduleName} in ${filePath}`); - return moduleCall; - } - } catch (error) { - this.logger.error(`Error reading tf file: ${filePath}`, error); - continue; - } - } - - return null; - } catch (error) { - this.logger.error(`Error finding module call: ${moduleName} in ${dir}`, error); - return null; + try { + // Find variable references in the value + const varRefs = this.parser.extractVariableReferences(value); + let resolvedValue = value; + + for (const varRef of varRefs) { + const resolvedRef = await this.resolveVariableValue( + varRef, + contextDir, + visited, + depth + ); + if (resolvedRef !== null) { + // Replace the variable reference with resolved value + const refPattern = new RegExp( + `\\b${varRef.replace(".", "\\.")}\\b`, + "g" + ); + resolvedValue = resolvedValue.replace(refPattern, resolvedRef); } + } + + return resolvedValue; + } catch (error) { + this.logger.error("Error resolving references in value", error); + return value; } + } + + private async findModuleCall( + moduleName: string, + dir: string + ): Promise { + try { + const cacheKey = `module:${moduleName}:${dir}`; + const cached = this.cache.get(cacheKey); + if (cached !== null) { + return JSON.parse(cached); + } + + if (!(await this.directoryExists(dir))) { + return null; + } + + const files = await readdir(dir); + const tfFiles = files.filter((f) => f.endsWith(".tf")); + + for (const file of tfFiles) { + const filePath = path.join(dir, file); - private async resolveModulePath(source: string, currentDir: string): Promise { try { - const cacheKey = `modulePath:${source}:${currentDir}`; - const cached = this.resolvedModulePaths.get(cacheKey); - if (cached) { - return cached; - } - - let resolvedPath: string | null = null; - - // Handle relative paths - if (source.startsWith('./') || source.startsWith('../')) { - const candidatePath = path.resolve(currentDir, source); - if (await this.directoryExists(candidatePath)) { - resolvedPath = candidatePath; - } - } - // Handle absolute paths within workspace - else if (source.startsWith('/')) { - const candidatePath = path.join(this.workspaceRoot, source); - if (await this.directoryExists(candidatePath)) { - resolvedPath = candidatePath; - } - } - // Handle registry modules (not supported for local resolution) - else if (source.includes('terraform.io') || source.includes('github.com')) { - this.logger.debug(`Registry module not supported for local resolution: ${source}`); - return null; - } - // Handle other relative paths - else { - const candidatePath = path.resolve(currentDir, source); - if (await this.directoryExists(candidatePath)) { - resolvedPath = candidatePath; - } - } - - if (resolvedPath && this.isWithinWorkspace(resolvedPath)) { - this.resolvedModulePaths.set(cacheKey, resolvedPath); - this.logger.debug(`Resolved module path: ${source} -> ${resolvedPath}`); - return resolvedPath; - } - - return null; + const content = await readFile(filePath, "utf8"); + const moduleCall = this.parser.parseModuleBlock(content, moduleName); + + if (moduleCall) { + this.cache.set(cacheKey, JSON.stringify(moduleCall)); + this.logger.debug(`Found module call ${moduleName} in ${filePath}`); + return moduleCall; + } } catch (error) { - this.logger.error(`Error resolving module path: ${source}`, error); - return null; + this.logger.error(`Error reading tf file: ${filePath}`, error); + continue; } + } + + return null; + } catch (error) { + this.logger.error( + `Error finding module call: ${moduleName} in ${dir}`, + error + ); + return null; } - - private async directoryExists(dirPath: string): Promise { - try { - await access(dirPath, fs.constants.F_OK); - const stats = await stat(dirPath); - return stats.isDirectory(); - } catch { - return false; + } + + private async resolveModulePath( + source: string, + currentDir: string + ): Promise { + try { + const cacheKey = `modulePath:${source}:${currentDir}`; + const cached = this.resolvedModulePaths.get(cacheKey); + if (cached) { + return cached; + } + + let resolvedPath: string | null = null; + + // Handle relative paths + if (source.startsWith("./") || source.startsWith("../")) { + const candidatePath = path.resolve(currentDir, source); + if (await this.directoryExists(candidatePath)) { + resolvedPath = candidatePath; } + } + // Handle absolute paths within workspace + else if (source.startsWith("/")) { + const candidatePath = path.join(this.workspaceRoot, source); + if (await this.directoryExists(candidatePath)) { + resolvedPath = candidatePath; + } + } + // Handle registry modules (not supported for local resolution) + else if ( + source.includes("terraform.io") || + source.includes("github.com") + ) { + this.logger.debug( + `Registry module not supported for local resolution: ${source}` + ); + return null; + } + // Handle other relative paths + else { + const candidatePath = path.resolve(currentDir, source); + if (await this.directoryExists(candidatePath)) { + resolvedPath = candidatePath; + } + } + + if (resolvedPath && this.isWithinWorkspace(resolvedPath)) { + this.resolvedModulePaths.set(cacheKey, resolvedPath); + this.logger.debug(`Resolved module path: ${source} -> ${resolvedPath}`); + return resolvedPath; + } + + return null; + } catch (error) { + this.logger.error(`Error resolving module path: ${source}`, error); + return null; } + } + + private async directoryExists(dirPath: string): Promise { + try { + await access(dirPath, fs.constants.F_OK); + const stats = await stat(dirPath); + return stats.isDirectory(); + } catch { + return false; + } + } + + private async findInLocals( + variableName: string, + dir: string + ): Promise { + try { + const cacheKey = `locals:${variableName}:${dir}`; + const cached = this.cache.get(cacheKey); + if (cached !== null) return cached; + + if (!(await this.directoryExists(dir))) { + return null; + } + + const files = await readdir(dir); + const tfFiles = files.filter((f) => f.endsWith(".tf")); + + for (const file of tfFiles) { + const filePath = path.join(dir, file); - private async findInLocals(variableName: string, dir: string): Promise { try { - const cacheKey = `locals:${variableName}:${dir}`; - const cached = this.cache.get(cacheKey); - if (cached !== null) return cached; - - if (!(await this.directoryExists(dir))) { - return null; - } - - const files = await readdir(dir); - const tfFiles = files.filter(f => f.endsWith('.tf')); - - for (const file of tfFiles) { - const filePath = path.join(dir, file); - - try { - const content = await readFile(filePath, 'utf8'); - const value = this.parser.parseLocalsBlock(content, variableName); - - if (value !== null) { - this.cache.set(cacheKey, value); - this.logger.debug(`Found local ${variableName} in ${filePath}`); - return value; - } - } catch (error) { - this.logger.error(`Error reading locale file: ${filePath}`, error); - continue; - } - } - - return null; + const content = await readFile(filePath, "utf8"); + const value = this.parser.parseLocalsBlock(content, variableName); + + if (value !== null) { + this.cache.set(cacheKey, value); + this.logger.debug(`Found local ${variableName} in ${filePath}`); + return value; + } } catch (error) { - this.logger.error(`Error searching locale files in ${dir}`, error); - return null; + this.logger.error(`Error reading locale file: ${filePath}`, error); + continue; } + } + + return null; + } catch (error) { + this.logger.error(`Error searching locale files in ${dir}`, error); + return null; } -} \ No newline at end of file + } +} diff --git a/src/utils/cache.ts b/src/utils/cache.ts index bbcadfc..07f805a 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,186 +1,200 @@ -import { Logger } from './logger'; +import { Logger } from "./logger"; interface CacheEntry { - value: string; - timestamp: number; - accessCount: number; - lastAccessed: number; + value: string; + timestamp: number; + accessCount: number; + lastAccessed: number; } export class TerraformCache { - private cache = new Map(); - private readonly TTL = 60000; // 60 seconds TTL - private readonly MAX_SIZE = 1000; // Maximum cache entries - private cleanupInterval: NodeJS.Timeout | null = null; - - constructor(private logger: Logger) { - this.startCleanupTimer(); + private cache = new Map(); + private readonly TTL = 60000; // 60 seconds TTL + private readonly MAX_SIZE = 1000; // Maximum cache entries + private cleanupInterval: NodeJS.Timeout | null = null; + + constructor(private logger: Logger) { + this.startCleanupTimer(); + } + + get(key: string): string | null { + try { + const entry = this.cache.get(key); + if (!entry) { + return null; + } + + const now = Date.now(); + + // Check if entry has expired + if (now - entry.timestamp > this.TTL) { + this.cache.delete(key); + this.logger.debug(`Cache entry expired: ${key}`); + return null; + } + + // Update access statistics + entry.accessCount++; + entry.lastAccessed = now; + + this.logger.debug(`Cache hit: ${key}`); + return entry.value; + } catch (error) { + this.logger.error(`Error getting cache entry: ${key}`, error); + return null; } - - get(key: string): string | null { - try { - const entry = this.cache.get(key); - if (!entry) { - return null; - } - - const now = Date.now(); - - // Check if entry has expired - if (now - entry.timestamp > this.TTL) { - this.cache.delete(key); - this.logger.debug(`Cache entry expired: ${key}`); - return null; - } - - // Update access statistics - entry.accessCount++; - entry.lastAccessed = now; - - this.logger.debug(`Cache hit: ${key}`); - return entry.value; - } catch (error) { - this.logger.error(`Error getting cache entry: ${key}`, error); - return null; - } + } + + set(key: string, value: string): void { + try { + // Check cache size limit + if (this.cache.size >= this.MAX_SIZE) { + this.evictLeastRecentlyUsed(); + } + + const now = Date.now(); + const entry: CacheEntry = { + value, + timestamp: now, + accessCount: 1, + lastAccessed: now, + }; + + this.cache.set(key, entry); + this.logger.debug(`Cache set: ${key}`); + } catch (error) { + this.logger.error(`Error setting cache entry: ${key}`, error); } + } - set(key: string, value: string): void { - try { - // Check cache size limit - if (this.cache.size >= this.MAX_SIZE) { - this.evictLeastRecentlyUsed(); - } - - const now = Date.now(); - const entry: CacheEntry = { - value, - timestamp: now, - accessCount: 1, - lastAccessed: now - }; - - this.cache.set(key, entry); - this.logger.debug(`Cache set: ${key}`); - } catch (error) { - this.logger.error(`Error setting cache entry: ${key}`, error); - } - } + invalidateFile(filePath: string): void { + try { + let deletedCount = 0; - invalidateFile(filePath: string): void { - try { - let deletedCount = 0; - - for (const [key] of this.cache) { - if (key.includes(filePath)) { - this.cache.delete(key); - deletedCount++; - } - } - - if (deletedCount > 0) { - this.logger.debug(`Invalidated ${deletedCount} cache entries for file: ${filePath}`); - } - } catch (error) { - this.logger.error(`Error invalidating cache for file: ${filePath}`, error); + for (const [key] of this.cache) { + if (key.includes(filePath)) { + this.cache.delete(key); + deletedCount++; } + } + + if (deletedCount > 0) { + this.logger.debug( + `Invalidated ${deletedCount} cache entries for file: ${filePath}` + ); + } + } catch (error) { + this.logger.error( + `Error invalidating cache for file: ${filePath}`, + error + ); } - - clear(): void { - try { - const size = this.cache.size; - this.cache.clear(); - this.logger.info(`Cache cleared, removed ${size} entries`); - } catch (error) { - this.logger.error('Error clearing cache', error); - } + } + + clear(): void { + try { + const size = this.cache.size; + this.cache.clear(); + this.logger.info(`Cache cleared, removed ${size} entries`); + } catch (error) { + this.logger.error("Error clearing cache", error); } - - size(): number { - return this.cache.size; + } + + size(): number { + return this.cache.size; + } + + getStats(): { + size: number; + hitRate: number; + oldestEntry: number; + newestEntry: number; + } { + const now = Date.now(); + let totalAccess = 0; + let oldestTimestamp = now; + let newestTimestamp = 0; + + for (const entry of this.cache.values()) { + totalAccess += entry.accessCount; + oldestTimestamp = Math.min(oldestTimestamp, entry.timestamp); + newestTimestamp = Math.max(newestTimestamp, entry.timestamp); } - getStats(): { size: number; hitRate: number; oldestEntry: number; newestEntry: number } { - const now = Date.now(); - let totalAccess = 0; - let oldestTimestamp = now; - let newestTimestamp = 0; - - for (const entry of this.cache.values()) { - totalAccess += entry.accessCount; - oldestTimestamp = Math.min(oldestTimestamp, entry.timestamp); - newestTimestamp = Math.max(newestTimestamp, entry.timestamp); - } - - const avgAccessPerEntry = this.cache.size > 0 ? totalAccess / this.cache.size : 0; - - return { - size: this.cache.size, - hitRate: Math.round(avgAccessPerEntry * 100) / 100, - oldestEntry: now - oldestTimestamp, - newestEntry: now - newestTimestamp - }; + const avgAccessPerEntry = + this.cache.size > 0 ? totalAccess / this.cache.size : 0; + + return { + size: this.cache.size, + hitRate: Math.round(avgAccessPerEntry * 100) / 100, + oldestEntry: now - oldestTimestamp, + newestEntry: now - newestTimestamp, + }; + } + + dispose(): void { + try { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + + this.clear(); + this.logger.debug("Cache disposed"); + } catch (error) { + this.logger.error("Error disposing cache", error); } - - dispose(): void { - try { - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = null; - } - - this.clear(); - this.logger.debug('Cache disposed'); - } catch (error) { - this.logger.error('Error disposing cache', error); + } + + private startCleanupTimer(): void { + // Run cleanup every 5 minutes + this.cleanupInterval = setInterval( + () => { + this.cleanup(); + }, + 5 * 60 * 1000 + ); + } + + private cleanup(): void { + try { + const now = Date.now(); + let expiredCount = 0; + + for (const [key, entry] of this.cache) { + if (now - entry.timestamp > this.TTL) { + this.cache.delete(key); + expiredCount++; } - } + } - private startCleanupTimer(): void { - // Run cleanup every 5 minutes - this.cleanupInterval = setInterval(() => { - this.cleanup(); - }, 5 * 60 * 1000); + if (expiredCount > 0) { + this.logger.debug(`Cleaned up ${expiredCount} expired cache entries`); + } + } catch (error) { + this.logger.error("Error during cache cleanup", error); } + } - private cleanup(): void { - try { - const now = Date.now(); - let expiredCount = 0; - - for (const [key, entry] of this.cache) { - if (now - entry.timestamp > this.TTL) { - this.cache.delete(key); - expiredCount++; - } - } - - if (expiredCount > 0) { - this.logger.debug(`Cleaned up ${expiredCount} expired cache entries`); - } - } catch (error) { - this.logger.error('Error during cache cleanup', error); - } - } + private evictLeastRecentlyUsed(): void { + try { + let lruKey: string | null = null; + let lruTimestamp = Date.now(); - private evictLeastRecentlyUsed(): void { - try { - let lruKey: string | null = null; - let lruTimestamp = Date.now(); - - for (const [key, entry] of this.cache) { - if (entry.lastAccessed < lruTimestamp) { - lruTimestamp = entry.lastAccessed; - lruKey = key; - } - } - - if (lruKey) { - this.cache.delete(lruKey); - this.logger.debug(`Evicted LRU cache entry: ${lruKey}`); - } - } catch (error) { - this.logger.error('Error during LRU eviction', error); + for (const [key, entry] of this.cache) { + if (entry.lastAccessed < lruTimestamp) { + lruTimestamp = entry.lastAccessed; + lruKey = key; } + } + + if (lruKey) { + this.cache.delete(lruKey); + this.logger.debug(`Evicted LRU cache entry: ${lruKey}`); + } + } catch (error) { + this.logger.error("Error during LRU eviction", error); } -} \ No newline at end of file + } +} diff --git a/src/utils/configurationManager.ts b/src/utils/configurationManager.ts index 67a1dfb..249f1e9 100644 --- a/src/utils/configurationManager.ts +++ b/src/utils/configurationManager.ts @@ -1,27 +1,31 @@ -import * as vscode from 'vscode'; +import * as vscode from "vscode"; export class ConfigurationManager { - private configSection = 'terraformResolver'; - private config: vscode.WorkspaceConfiguration; + private configSection = "terraformResolver"; + private config: vscode.WorkspaceConfiguration; - constructor() { - this.config = vscode.workspace.getConfiguration(this.configSection); - } + constructor() { + this.config = vscode.workspace.getConfiguration(this.configSection); + } - // Lädt die Konfiguration neu (z.B. bei onDidChangeConfiguration) - reload() { - this.config = vscode.workspace.getConfiguration(this.configSection); - } + // Lädt die Konfiguration neu (z.B. bei onDidChangeConfiguration) + reload() { + this.config = vscode.workspace.getConfiguration(this.configSection); + } - // Prüft, ob die Inlay-Hints aktiviert sind - isEnabled(): boolean { - return this.config.get('enabled', true); - } + // Prüft, ob die Inlay-Hints aktiviert sind + isEnabled(): boolean { + return this.config.get("enabled", true); + } - // Aktiviert/deaktiviert Inlay-Hints (Speicherung in den Workspace oder User settings) - async setEnabled(enabled: boolean): Promise { - // Hier z.B. Workspace-Einstellung (kann auch global geändert werden) - await this.config.update('enabled', enabled, vscode.ConfigurationTarget.Workspace); - this.reload(); - } + // Aktiviert/deaktiviert Inlay-Hints (Speicherung in den Workspace oder User settings) + async setEnabled(enabled: boolean): Promise { + // Hier z.B. Workspace-Einstellung (kann auch global geändert werden) + await this.config.update( + "enabled", + enabled, + vscode.ConfigurationTarget.Workspace + ); + this.reload(); + } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index a801e6b..8369c77 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,117 +1,121 @@ -import * as vscode from 'vscode'; +import * as vscode from "vscode"; export enum LogLevel { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3 + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, } export class Logger { - private outputChannel: vscode.OutputChannel; - private logLevel: LogLevel = LogLevel.INFO; + private outputChannel: vscode.OutputChannel; + private logLevel: LogLevel = LogLevel.INFO; - constructor(private name: string) { - this.outputChannel = vscode.window.createOutputChannel(`Terraform Variable Resolver - ${name}`); - this.loadConfiguration(); - } + constructor(private name: string) { + this.outputChannel = vscode.window.createOutputChannel( + `Terraform Variable Resolver - ${name}` + ); + this.loadConfiguration(); + } - private loadConfiguration(): void { - try { - const config = vscode.workspace.getConfiguration('terraformResolver'); - const configLevel = config.get('logLevel', 'info').toLowerCase(); - - switch (configLevel) { - case 'debug': - this.logLevel = LogLevel.DEBUG; - break; - case 'warn': - this.logLevel = LogLevel.WARN; - break; - case 'error': - this.logLevel = LogLevel.ERROR; - break; - default: - this.logLevel = LogLevel.INFO; - } - } catch (error) { - console.error('Failed to load logger configuration:', error); - } - } + private loadConfiguration(): void { + try { + const config = vscode.workspace.getConfiguration("terraformResolver"); + const configLevel = config.get("logLevel", "info").toLowerCase(); - debug(message: string, ...args: any[]): void { - this.log(LogLevel.DEBUG, message, ...args); + switch (configLevel) { + case "debug": + this.logLevel = LogLevel.DEBUG; + break; + case "warn": + this.logLevel = LogLevel.WARN; + break; + case "error": + this.logLevel = LogLevel.ERROR; + break; + default: + this.logLevel = LogLevel.INFO; + } + } catch (error) { + console.error("Failed to load logger configuration:", error); } + } - info(message: string, ...args: any[]): void { - this.log(LogLevel.INFO, message, ...args); - } + debug(message: string, ...args: any[]): void { + this.log(LogLevel.DEBUG, message, ...args); + } - warn(message: string, ...args: any[]): void { - this.log(LogLevel.WARN, message, ...args); - } + info(message: string, ...args: any[]): void { + this.log(LogLevel.INFO, message, ...args); + } + + warn(message: string, ...args: any[]): void { + this.log(LogLevel.WARN, message, ...args); + } - error(message: string, error?: any, ...args: any[]): void { - if (error) { - args.unshift(error); - } - this.log(LogLevel.ERROR, message, ...args); + error(message: string, error?: any, ...args: any[]): void { + if (error) { + args.unshift(error); } + this.log(LogLevel.ERROR, message, ...args); + } - show(): void { - this.outputChannel.show(); + show(): void { + this.outputChannel.show(); + } + + dispose(): void { + try { + this.outputChannel.dispose(); + } catch (error) { + console.error("Error disposing logger:", error); } + } - dispose(): void { - try { - this.outputChannel.dispose(); - } catch (error) { - console.error('Error disposing logger:', error); - } + private log(level: LogLevel, message: string, ...args: any[]): void { + if (level < this.logLevel) { + return; } - private log(level: LogLevel, message: string, ...args: any[]): void { - if (level < this.logLevel) { - return; - } - - try { - const timestamp = new Date().toISOString(); - const levelStr = LogLevel[level]; - const prefix = `[${timestamp}] [${levelStr}] [${this.name}]`; - - let logMessage = `${prefix} ${message}`; - - if (args.length > 0) { - const formattedArgs = args.map(arg => { - if (arg instanceof Error) { - return `\n Error: ${arg.message}\n Stack: ${arg.stack}`; - } else if (typeof arg === 'object') { - try { - return `\n ${JSON.stringify(arg, null, 2)}`; - } catch { - return `\n ${String(arg)}`; - } - } else { - return String(arg); - } - }).join(' '); - - logMessage += ` ${formattedArgs}`; - } + try { + const timestamp = new Date().toISOString(); + const levelStr = LogLevel[level]; + const prefix = `[${timestamp}] [${levelStr}] [${this.name}]`; - this.outputChannel.appendLine(logMessage); + let logMessage = `${prefix} ${message}`; - // Also log to console for development - if (level >= LogLevel.ERROR) { - console.error(logMessage); - } else if (level >= LogLevel.WARN) { - console.warn(logMessage); - } else if (this.logLevel <= LogLevel.DEBUG) { - console.log(logMessage); + if (args.length > 0) { + const formattedArgs = args + .map((arg) => { + if (arg instanceof Error) { + return `\n Error: ${arg.message}\n Stack: ${arg.stack}`; + } else if (typeof arg === "object") { + try { + return `\n ${JSON.stringify(arg, null, 2)}`; + } catch { + return `\n ${String(arg)}`; + } + } else { + return String(arg); } - } catch (error) { - console.error('Logger failed to write message:', error); - } + }) + .join(" "); + + logMessage += ` ${formattedArgs}`; + } + + this.outputChannel.appendLine(logMessage); + + // Also log to console for development + if (level >= LogLevel.ERROR) { + console.error(logMessage); + } else if (level >= LogLevel.WARN) { + console.warn(logMessage); + } else if (this.logLevel <= LogLevel.DEBUG) { + console.log(logMessage); + } + } catch (error) { + console.error("Logger failed to write message:", error); } -} \ No newline at end of file + } +} diff --git a/src/utils/performanceMonitor.ts b/src/utils/performanceMonitor.ts index 41d2a6c..38a9562 100644 --- a/src/utils/performanceMonitor.ts +++ b/src/utils/performanceMonitor.ts @@ -1,72 +1,74 @@ -import { Logger } from './logger'; +import { Logger } from "./logger"; export class PerformanceMonitor { - private logger: Logger; - private timers: Map; + private logger: Logger; + private timers: Map; - constructor(logger: Logger) { - this.logger = logger; - this.timers = new Map(); - } + constructor(logger: Logger) { + this.logger = logger; + this.timers = new Map(); + } - startTimer(name: string): Timer { - const timer = new Timer(name, this.logger); - this.timers.set(name, timer); - timer.start(); - return timer; - } + startTimer(name: string): Timer { + const timer = new Timer(name, this.logger); + this.timers.set(name, timer); + timer.start(); + return timer; + } - stopTimer(name: string): void { - const timer = this.timers.get(name); - if (timer) { - timer.stop(); - this.timers.delete(name); - } + stopTimer(name: string): void { + const timer = this.timers.get(name); + if (timer) { + timer.stop(); + this.timers.delete(name); } + } - dispose(): void { - // Stoppe alle Timer wenn nötig - for (const timer of this.timers.values()) { - timer.stop(); - } - this.timers.clear(); + dispose(): void { + // Stoppe alle Timer wenn nötig + for (const timer of this.timers.values()) { + timer.stop(); } + this.timers.clear(); + } } export class Timer { - private name: string; - private logger: Logger; - private startTime: [number, number] | null = null; - private endTime: [number, number] | null = null; - private durationMs: number | null = null; + private name: string; + private logger: Logger; + private startTime: [number, number] | null = null; + private endTime: [number, number] | null = null; + private durationMs: number | null = null; - constructor(name: string, logger: Logger) { - this.name = name; - this.logger = logger; - } + constructor(name: string, logger: Logger) { + this.name = name; + this.logger = logger; + } - start(): void { - this.startTime = process.hrtime(); - } + start(): void { + this.startTime = process.hrtime(); + } - stop(): void { - if (!this.startTime) { - this.logger.warn(`Timer "${this.name}" stopped without being started.`); - return; - } - this.endTime = process.hrtime(this.startTime); - this.durationMs = (this.endTime[0] * 1000) + (this.endTime[1] / 1e6); - this.logger.debug(`Timer "${this.name}" took ${this.durationMs.toFixed(2)} ms.`); + stop(): void { + if (!this.startTime) { + this.logger.warn(`Timer "${this.name}" stopped without being started.`); + return; } + this.endTime = process.hrtime(this.startTime); + this.durationMs = this.endTime[0] * 1000 + this.endTime[1] / 1e6; + this.logger.debug( + `Timer "${this.name}" took ${this.durationMs.toFixed(2)} ms.` + ); + } - getDuration(): number { - if (this.durationMs !== null) { - return this.durationMs; - } - if (this.startTime) { - const diff = process.hrtime(this.startTime); - return (diff[0] * 1000) + (diff[1] / 1e6); - } - return 0; + getDuration(): number { + if (this.durationMs !== null) { + return this.durationMs; + } + if (this.startTime) { + const diff = process.hrtime(this.startTime); + return diff[0] * 1000 + diff[1] / 1e6; } + return 0; + } } diff --git a/tsconfig.json b/tsconfig.json index 356580f..b2fc4ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,15 @@ { - "compilerOptions": { - "module": "Node16", - "target": "ES2022", - "outDir": "out", - "lib": [ - "ES2022" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true, /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "outDir": "out", + "lib": ["ES2022"], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */ + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + } }