From 535a767bf66739e7c1e57c6e3550507c8027b8b4 Mon Sep 17 00:00:00 2001 From: Conceicao Date: Wed, 6 May 2026 15:05:00 -0300 Subject: [PATCH] feat: add support for variable scope resolution in COBOL methods, it is now possible to add variables to the watch across different programs --- defaultDebugConfigs.json | 4 +- .../CobolEvaluatableExpressionProvider.ts | 80 ++++++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/defaultDebugConfigs.json b/defaultDebugConfigs.json index 90f1bf1..668ef22 100644 --- a/defaultDebugConfigs.json +++ b/defaultDebugConfigs.json @@ -47,7 +47,9 @@ "unexpected\\s+error\\s+usage", "syntax\\s+error", "Error:\\s+dynamic\\s+item\\s+not\\s+allocated\\s+", - "Dynamic\\s+item\\s+not\\s+allocated\\s+" + "Dynamic\\s+item\\s+not\\s+allocated\\s+", + "no\\s+such\\s+method\\s+", + "no\\s+such\\s+program\\s+" ] }, "changeVariableValue": { diff --git a/src/provider/CobolEvaluatableExpressionProvider.ts b/src/provider/CobolEvaluatableExpressionProvider.ts index 1fa3904..db90f26 100644 --- a/src/provider/CobolEvaluatableExpressionProvider.ts +++ b/src/provider/CobolEvaluatableExpressionProvider.ts @@ -1,4 +1,5 @@ import { EvaluatableExpressionProvider, TextDocument, Position, EvaluatableExpression, ProviderResult, window, Range } from "vscode"; +import * as path from "path"; /** Max column index to retrieve line content */ const MAX_COLUMN_INDEX = 300; @@ -16,14 +17,89 @@ export class CobolEvaluatableExpressionProvider implements EvaluatableExpression } const selectionRange = this.getSelectionRangeInEditor(); if (selectionRange) { - return new EvaluatableExpression(selectionRange); + const variableName = document.getText(selectionRange); + const expression = this.buildExpression(variableName, document, position); + return new EvaluatableExpression(selectionRange, expression); } const wordRange = document.getWordRangeAtPosition(position) if (!wordRange) { return undefined; } const trnsformedWordRange = this.transformWordRange(document, wordRange, position.line + 1); - return new EvaluatableExpression(trnsformedWordRange); + const variableName = document.getText(trnsformedWordRange); + const expression = this.buildExpression(variableName, document, position); + return new EvaluatableExpression(trnsformedWordRange, expression); + } + + /** + * Builds the display expression for a variable, adding -c PROGRAM or -c PROGRAM:>METHOD + * prefix so the value is always resolved in the context of the correct scope. + * Method scope is only added when the variable declaration is found inside the method block. + */ + private buildExpression(variableName: string, document: TextDocument, position: Position): string { + const programName = path.parse(document.fileName).name.toUpperCase(); + const methodName = this.findMethodScopeForVariable(document, position, variableName); + if (methodName) { + return `-c ${programName}:>${methodName} ${variableName}`; + } + return `-c ${programName} ${variableName}`; + } + + /** + * Determines if a variable belongs to a method scope by: + * 1. Searching backwards from the cursor for the nearest enclosing method-id (fast boundary). + * 2. If inside a method, searching for the variable declaration only within that method block. + * If found → method scope; if not found → program scope. + */ + private findMethodScopeForVariable(document: TextDocument, position: Position, variableName: string): string | undefined { + const method = this.findEnclosingMethod(document, position.line); + if (!method) { + return undefined; + } + const parenIdx = variableName.indexOf('('); + const baseName = parenIdx >= 0 ? variableName.substring(0, parenIdx).trim() : variableName; + return this.isDeclaredBetween(document, method.startLine, position.line, baseName) ? method.name : undefined; + } + + /** + * Searches backwards from fromLine to find the nearest enclosing method-id. + * Returns the method start line and resolved name (alias preferred), or undefined if in program scope. + */ + private findEnclosingMethod(document: TextDocument, fromLine: number): { startLine: number; name: string } | undefined { + const METHOD_NAME_GROUP = 1; + const METHOD_ALIAS_GROUP = 2; + const METHOD_ID_PATTERN = /\bmethod-id\s*\.\s*([a-zA-Z0-9_\-]+)(?:\s*\([^)]*\))?(?:\s+as\s+"([^"]+)")?/i; + const END_METHOD_PATTERN = /\bend\s+method\b/i; + for (let line = fromLine; line >= 0; line--) { + const lineText = document.lineAt(line).text; + if (END_METHOD_PATTERN.test(lineText) && line < fromLine) { + return undefined; + } + const match = METHOD_ID_PATTERN.exec(lineText); + if (match) { + const name = match[METHOD_ALIAS_GROUP] !== undefined ? match[METHOD_ALIAS_GROUP] : match[METHOD_NAME_GROUP]; + return { startLine: line, name }; + } + } + return undefined; + } + + /** + * Checks whether a variable declaration exists between startLine and endLine (inclusive). + * Recognizes level-number declarations, declare statements, and perform varying inline declarations. + */ + private isDeclaredBetween(document: TextDocument, startLine: number, endLine: number, variableName: string): boolean { + const escaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const levelPattern = new RegExp(`^\\s*\\d+\\s+${escaped}[\\s,.]`, 'i'); + const declarePattern = new RegExp(`\\bdeclare\\s+${escaped}\\b`, 'i'); + const varyingPattern = new RegExp(`\\bvarying\\s+${escaped}\\s+as\\b`, 'i'); + for (let line = startLine; line <= endLine; line++) { + const text = document.lineAt(line).text; + if (levelPattern.test(text) || declarePattern.test(text) || varyingPattern.test(text)) { + return true; + } + } + return false; } /**