diff --git a/TypeCobol.LanguageServer.Test/RefactoringTest.cs b/TypeCobol.LanguageServer.Test/RefactoringTest.cs index 54cbcd458..2f6674193 100644 --- a/TypeCobol.LanguageServer.Test/RefactoringTest.cs +++ b/TypeCobol.LanguageServer.Test/RefactoringTest.cs @@ -4,7 +4,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using TypeCobol.Compiler; using TypeCobol.Compiler.Directives; -using TypeCobol.LanguageServer.Commands.Refactor; +using TypeCobol.LanguageServer.Commands; using TypeCobol.LanguageServer.Test.RefactoringTests; using TypeCobol.LanguageServer.VsCodeProtocol; using TypeCobol.Test.Utils; @@ -71,6 +71,8 @@ public FixedTextEditGenerator() public string ExpectedResult { get; } + public IEnvironmentVariableProvider EnvironmentVariableProvider { get; set; } + public TextDocumentIdentifier PrepareRefactoring(object[] arguments) { return new TextDocumentIdentifier(){ uri = nameof(FixedTextEditGenerator) }; @@ -123,8 +125,8 @@ private static void TestDirectory([CallerMemberName] string directoryName = null string path = Path.GetFullPath("RefactoringTests"); path = Path.Combine(path, directoryName); - var failedTests = new List<(string TestName, Exception Exception)>(); - foreach (var testFile in Directory.GetFiles(path)) + var failedTests = new List<(string TestFile, Exception Exception)>(); + foreach (var testFile in Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)) { try { @@ -133,8 +135,7 @@ private static void TestDirectory([CallerMemberName] string directoryName = null } catch (Exception exception) { - string testName = Path.GetFileNameWithoutExtension(testFile); - failedTests.Add((testName, exception)); + failedTests.Add((testFile, exception)); } } @@ -144,7 +145,7 @@ private static void TestDirectory([CallerMemberName] string directoryName = null Console.WriteLine(); foreach (var failedTest in failedTests) { - Console.WriteLine($"[{failedTest.TestName}] failed !"); + Console.WriteLine($"[{failedTest.TestFile}] failed !"); Console.WriteLine(failedTest.Exception.Message); Console.WriteLine(); } @@ -164,5 +165,8 @@ public void TestServerSideApplyEdit() [TestMethod] public void AdjustFillers() => TestDirectory(); + + [TestMethod] + public void InsertVariableDisplay() => TestDirectory(); } } diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/AdjustFillers/SimpleTest.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/AdjustFillers/SimpleTest.cbl index 4e86ecead..c32c97f4c 100644 --- a/TypeCobol.LanguageServer.Test/RefactoringTests/AdjustFillers/SimpleTest.cbl +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/AdjustFillers/SimpleTest.cbl @@ -18,11 +18,11 @@ GOBACK . END PROGRAM TCOMFL06. ---------------------------------------------------------------------------- -TypeCobol.LanguageServer.Commands.Refactor.AdjustFillerRefactoringProcessor ---------------------------------------------------------------------------- +--------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.AdjustFillers.AdjustFillersRefactoringProcessor +--------------------------------------------------------------------------------- [{"uri":"file:/test.expected.cbl"}] ---------------------------------------------------------------------------- +--------------------------------------------------------------------------------- refactoring.label=Adjust FILLERs: 3 FILLER(s) modified refactoring.source= IDENTIFICATION DIVISION. diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/National/SimpleTest.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/National/SimpleTest.cbl new file mode 100644 index 000000000..e65cd627c --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/National/SimpleTest.cbl @@ -0,0 +1,42 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var1 PIC N(200). + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 5, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 0, "name": "var1" + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var1 PIC N(200). + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY 'var1 <' FUNCTION DISPLAY-OF (var1) '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/National/WithGroups.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/National/WithGroups.cbl new file mode 100644 index 000000000..e3fc2f59f --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/National/WithGroups.cbl @@ -0,0 +1,83 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC N. + 10 var2 PIC N. + 01 group2. + 05 group2-1. + 10 var3 PIC N. + 10 var4 PIC N. + 10 var5 PIC N. + 05 group2-2. + 10 var6 PIC N. + 10 var7 PIc N. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 16, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "group1", "ch": [ + { + "vm": 1, "name": "group1-1", "ch": [ + { + "vm": 0, "name": "var1" + } + ] + } + ] + }, + { + "vm": 2, "name": "group2" + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC N. + 10 var2 PIC N. + 01 group2. + 05 group2-1. + 10 var3 PIC N. + 10 var4 PIC N. + 10 var5 PIC N. + 05 group2-2. + 10 var6 PIC N. + 10 var7 PIc N. + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY ' var1 <' FUNCTION DISPLAY-OF (var1) '>' + D DISPLAY 'group2' + D DISPLAY ' group2-1' + D DISPLAY ' var3 <' FUNCTION DISPLAY-OF (var3) '>' + D DISPLAY ' var4 <' FUNCTION DISPLAY-OF (var4) '>' + D DISPLAY ' var5 <' FUNCTION DISPLAY-OF (var5) '>' + D DISPLAY ' group2-2' + D DISPLAY ' var6 <' FUNCTION DISPLAY-OF (var6) '>' + D DISPLAY ' var7 <' FUNCTION DISPLAY-OF (var7) '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInAnonymousOccurs.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInAnonymousOccurs.cbl new file mode 100644 index 000000000..f8830e6b2 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInAnonymousOccurs.cbl @@ -0,0 +1,88 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 OCCURS 1000. + 10 OCCURS 200. + 15 OCCURS 30. + 20 var1 PIC X. + 20 FILLER PIC X(20). + 20 var2 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 11, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "three-levels", "ch": [ + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + } + ] + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 OCCURS 1000. + 10 OCCURS 200. + 15 OCCURS 30. + 20 var1 PIC X. + 20 FILLER PIC X(20). + 20 var2 PIC X. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D77 Idx-d4df4249-1 PIC 9(4) COMP-5. + D77 Idx-d4df4249-2 PIC 9(4) COMP-5. + D77 Idx-d4df4249-3 PIC 9(4) COMP-5. + * + + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > 1000 + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > 200 + D PERFORM VARYING Idx-d4df4249-3 FROM 1 BY 1 UNTIL + D Idx-d4df4249-3 > 30 + D DISPLAY ' FILLER (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') <' three-levels (2 + D + (Idx-d4df4249-3 - 1) * 22 + (Idx-d4df4249-2 - 1) * + D 660 + (Idx-d4df4249-1 - 1) * 132000:20) '>' + D END-PERFORM + D END-PERFORM + D END-PERFORM + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInOccursNoDependingOn.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInOccursNoDependingOn.cbl new file mode 100644 index 000000000..fca015c02 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInOccursNoDependingOn.cbl @@ -0,0 +1,88 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 root-table-3 OCCURS 1000. + 10 table-level1-2 OCCURS 200. + 15 table-level2-1 OCCURS 30. + 20 var1 PIC X. + 20 FILLER PIC X(20). + 20 var2 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 11, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "three-levels", "ch": [ + { + "vm": 1, "name": "root-table-3", "ch": [ + { + "vm": 1, "name": "table-level1-2", "ch": [ + { + "vm": 1, "name": "table-level2-1", "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + } + ] + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 root-table-3 OCCURS 1000. + 10 table-level1-2 OCCURS 200. + 15 table-level2-1 OCCURS 30. + 20 var1 PIC X. + 20 FILLER PIC X(20). + 20 var2 PIC X. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D77 Idx-d4df4249-1 PIC 9(4) COMP-5. + D77 Idx-d4df4249-2 PIC 9(4) COMP-5. + D77 Idx-d4df4249-3 PIC 9(4) COMP-5. + * + + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > 1000 + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > 200 + D PERFORM VARYING Idx-d4df4249-3 FROM 1 BY 1 UNTIL + D Idx-d4df4249-3 > 30 + D DISPLAY ' FILLER (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') (2:20) <' + D table-level2-1 (Idx-d4df4249-1 Idx-d4df4249-2 + D Idx-d4df4249-3) (2:20) '>' + D END-PERFORM + D END-PERFORM + D END-PERFORM + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInOccursWithDependingOn.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInOccursWithDependingOn.cbl new file mode 100644 index 000000000..ff4e784df --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/FillersInOccursWithDependingOn.cbl @@ -0,0 +1,117 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 root-table-3-count PIC S9(4) BINARY. + 05 table-level1-2-count PIC S9(3) BINARY. + 05 table-level2-1-count PIC S9(2) BINARY. + 05 root-table-3 OCCURS 1000 + DEPENDING ON root-table-3-count. + 10 table-level1-2 OCCURS 200 + DEPENDING ON table-level1-2-count. + 15 table-level2-1 OCCURS 30 + DEPENDING ON table-level2-1-count. + 20 var1 PIC X. + 20 FILLER PIC X(20). + 20 var2 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 17, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "three-levels", "ch": [ + { + "vm": 1, "name": "root-table-3", "ch": [ + { + "vm": 1, "name": "table-level1-2", "ch": [ + { + "vm": 1, "name": "table-level2-1", "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + } + ] + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 root-table-3-count PIC S9(4) BINARY. + 05 table-level1-2-count PIC S9(3) BINARY. + 05 table-level2-1-count PIC S9(2) BINARY. + 05 root-table-3 OCCURS 1000 + DEPENDING ON root-table-3-count. + 10 table-level1-2 OCCURS 200 + DEPENDING ON table-level1-2-count. + 15 table-level2-1 OCCURS 30 + DEPENDING ON table-level2-1-count. + 20 var1 PIC X. + 20 FILLER PIC X(20). + 20 var2 PIC X. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D77 Idx-d4df4249-1 PIC 9(4) COMP-5. + D77 Idx-d4df4249-2 PIC 9(4) COMP-5. + D77 Idx-d4df4249-3 PIC 9(4) COMP-5. + * + + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D IF root-table-3-count >= 1 AND <= 1000 + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > root-table-3-count + D IF table-level1-2-count >= 1 AND <= 200 + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > table-level1-2-count + D IF table-level2-1-count >= 1 AND <= 30 + D PERFORM VARYING Idx-d4df4249-3 FROM 1 BY 1 UNTIL + D Idx-d4df4249-3 > table-level2-1-count + D DISPLAY ' FILLER (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') (2:20) <' + D table-level2-1 (Idx-d4df4249-1 Idx-d4df4249-2 + D Idx-d4df4249-3) (2:20) '>' + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "table-level2-1" because i' + D 'ts DEPENDING ON "table-level2-1-count" is' + D ' out of range.' + D END-IF + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "table-level1-2" because its D' + D 'EPENDING ON "table-level1-2-count" is out of ' + D 'range.' + D END-IF + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "root-table-3" because its DEPENDI' + D 'NG ON "root-table-3-count" is out of range.' + D END-IF + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/NationalInOccursNoDependingOn.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/NationalInOccursNoDependingOn.cbl new file mode 100644 index 000000000..4a612c1d4 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/NationalInOccursNoDependingOn.cbl @@ -0,0 +1,88 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 root-table-3 OCCURS 1000. + 10 table-level1-2 OCCURS 200. + 15 table-level2-1 OCCURS 30. + 20 var1 PIC X. + 20 var-national PIC N(20). + 20 var2 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 11, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "three-levels", "ch": [ + { + "vm": 1, "name": "root-table-3", "ch": [ + { + "vm": 1, "name": "table-level1-2", "ch": [ + { + "vm": 1, "name": "table-level2-1", "ch": [ + { + "vm": 0, "name": "var-national" + } + ] + } + ] + } + ] + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 root-table-3 OCCURS 1000. + 10 table-level1-2 OCCURS 200. + 15 table-level2-1 OCCURS 30. + 20 var1 PIC X. + 20 var-national PIC N(20). + 20 var2 PIC X. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D77 Idx-d4df4249-1 PIC 9(4) COMP-5. + D77 Idx-d4df4249-2 PIC 9(4) COMP-5. + D77 Idx-d4df4249-3 PIC 9(4) COMP-5. + * + + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > 1000 + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > 200 + D PERFORM VARYING Idx-d4df4249-3 FROM 1 BY 1 UNTIL + D Idx-d4df4249-3 > 30 + D DISPLAY ' var-national (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') <' FUNCTION + D DISPLAY-OF (var-national (Idx-d4df4249-1 Idx-d4df4249-2 + D Idx-d4df4249-3)) '>' + D END-PERFORM + D END-PERFORM + D END-PERFORM + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/NationalInOccursWithDependingOn.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/NationalInOccursWithDependingOn.cbl new file mode 100644 index 000000000..615ce7959 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/NationalInOccursWithDependingOn.cbl @@ -0,0 +1,117 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 root-table-3-count PIC S9(4) BINARY. + 05 table-level1-2-count PIC S9(3) BINARY. + 05 table-level2-1-count PIC S9(2) BINARY. + 05 root-table-3 OCCURS 1000 + DEPENDING ON root-table-3-count. + 10 table-level1-2 OCCURS 200 + DEPENDING ON table-level1-2-count. + 15 table-level2-1 OCCURS 30 + DEPENDING ON table-level2-1-count. + 20 var1 PIC X. + 20 var-national PIC N(20). + 20 var2 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 17, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "three-levels", "ch": [ + { + "vm": 1, "name": "root-table-3", "ch": [ + { + "vm": 1, "name": "table-level1-2", "ch": [ + { + "vm": 1, "name": "table-level2-1", "ch": [ + { + "vm": 0, "name": "var-national" + } + ] + } + ] + } + ] + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 three-levels. + 05 root-table-3-count PIC S9(4) BINARY. + 05 table-level1-2-count PIC S9(3) BINARY. + 05 table-level2-1-count PIC S9(2) BINARY. + 05 root-table-3 OCCURS 1000 + DEPENDING ON root-table-3-count. + 10 table-level1-2 OCCURS 200 + DEPENDING ON table-level1-2-count. + 15 table-level2-1 OCCURS 30 + DEPENDING ON table-level2-1-count. + 20 var1 PIC X. + 20 var-national PIC N(20). + 20 var2 PIC X. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D77 Idx-d4df4249-1 PIC 9(4) COMP-5. + D77 Idx-d4df4249-2 PIC 9(4) COMP-5. + D77 Idx-d4df4249-3 PIC 9(4) COMP-5. + * + + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D IF root-table-3-count >= 1 AND <= 1000 + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > root-table-3-count + D IF table-level1-2-count >= 1 AND <= 200 + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > table-level1-2-count + D IF table-level2-1-count >= 1 AND <= 30 + D PERFORM VARYING Idx-d4df4249-3 FROM 1 BY 1 UNTIL + D Idx-d4df4249-3 > table-level2-1-count + D DISPLAY ' var-national (' Idx-d4df4249-1 + D ' ' Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') <' + D FUNCTION DISPLAY-OF (var-national (Idx-d4df4249-1 + D Idx-d4df4249-2 Idx-d4df4249-3)) '>' + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "table-level2-1" because i' + D 'ts DEPENDING ON "table-level2-1-count" is' + D ' out of range.' + D END-IF + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "table-level1-2" because its D' + D 'EPENDING ON "table-level1-2-count" is out of ' + D 'range.' + D END-IF + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "root-table-3" because its DEPENDI' + D 'NG ON "root-table-3-count" is out of range.' + D END-IF + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/OccursNoDependingOn.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/OccursNoDependingOn.cbl new file mode 100644 index 000000000..df12e80fe --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/OccursNoDependingOn.cbl @@ -0,0 +1,125 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 one-level. + 05 root-table-1 OCCURS 10. + 10 var1 PIC X. + 10 var2 PIC X. + 01 two-levels. + 05 root-table-2 OCCURS 100. + 10 table-level1-1 OCCURS 20. + 15 var3 PIC X. + 15 var4 PIC X. + 01 three-levels. + 05 root-table-3 OCCURS 1000. + 10 table-level1-2 OCCURS 200. + 15 table-level2-1 OCCURS 30. + 20 var5 PIC X. + 20 var6 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 19, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 2, "name": "one-level" + }, + { + "vm": 2, "name": "two-levels" + }, + { + "vm": 2, "name": "three-levels" + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 one-level. + 05 root-table-1 OCCURS 10. + 10 var1 PIC X. + 10 var2 PIC X. + 01 two-levels. + 05 root-table-2 OCCURS 100. + 10 table-level1-1 OCCURS 20. + 15 var3 PIC X. + 15 var4 PIC X. + 01 three-levels. + 05 root-table-3 OCCURS 1000. + 10 table-level1-2 OCCURS 200. + 15 table-level2-1 OCCURS 30. + 20 var5 PIC X. + 20 var6 PIC X. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D77 Idx-d4df4249-1 PIC 9(4) COMP-5. + D77 Idx-d4df4249-2 PIC 9(4) COMP-5. + D77 Idx-d4df4249-3 PIC 9(4) COMP-5. + * + + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY 'one-level' + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > 10 + D DISPLAY ' root-table-1 (' Idx-d4df4249-1 ')' + D DISPLAY ' var1 (' Idx-d4df4249-1 ') <' var1 + D (Idx-d4df4249-1) '>' + D DISPLAY ' var2 (' Idx-d4df4249-1 ') <' var2 + D (Idx-d4df4249-1) '>' + D END-PERFORM + D DISPLAY 'two-levels' + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > 100 + D DISPLAY ' root-table-2 (' Idx-d4df4249-1 ')' + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > 20 + D DISPLAY ' table-level1-1 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ')' + D DISPLAY ' var3 (' Idx-d4df4249-1 ' ' Idx-d4df4249-2 + D ') <' var3 (Idx-d4df4249-1 Idx-d4df4249-2) '>' + D DISPLAY ' var4 (' Idx-d4df4249-1 ' ' Idx-d4df4249-2 + D ') <' var4 (Idx-d4df4249-1 Idx-d4df4249-2) '>' + D END-PERFORM + D END-PERFORM + D DISPLAY 'three-levels' + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > 1000 + D DISPLAY ' root-table-3 (' Idx-d4df4249-1 ')' + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > 200 + D DISPLAY ' table-level1-2 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ')' + D PERFORM VARYING Idx-d4df4249-3 FROM 1 BY 1 UNTIL + D Idx-d4df4249-3 > 30 + D DISPLAY ' table-level2-1 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ')' + D DISPLAY ' var5 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') <' var5 + D (Idx-d4df4249-1 Idx-d4df4249-2 Idx-d4df4249-3) '>' + D DISPLAY ' var6 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') <' var6 + D (Idx-d4df4249-1 Idx-d4df4249-2 Idx-d4df4249-3) '>' + D END-PERFORM + D END-PERFORM + D END-PERFORM + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/OccursWithDependingOn.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/OccursWithDependingOn.cbl new file mode 100644 index 000000000..c14c67ff6 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/Occurs/OccursWithDependingOn.cbl @@ -0,0 +1,192 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 one-level. + 05 root-table-1-count PIC S9(2) BINARY. + 05 root-table-1 OCCURS 10 + DEPENDING ON root-table-1-count. + 10 var1 PIC X. + 10 var2 PIC X. + 01 two-levels. + 05 root-table-2-count PIC S9(3) BINARY. + 05 table-level1-1-count PIC S9(2) BINARY. + 05 root-table-2 OCCURS 100 + DEPENDING ON root-table-2-count. + 10 table-level1-1 OCCURS 20 + DEPENDING ON table-level1-1-count. + 15 var3 PIC X. + 15 var4 PIC X. + 01 three-levels. + 05 root-table-3-count PIC S9(4) BINARY. + 05 table-level1-2-count PIC S9(3) BINARY. + 05 table-level2-1-count PIC S9(2) BINARY. + 05 root-table-3 OCCURS 1000 + DEPENDING ON root-table-3-count. + 10 table-level1-2 OCCURS 200 + DEPENDING ON table-level1-2-count. + 15 table-level2-1 OCCURS 30 + DEPENDING ON table-level2-1-count. + 20 var5 PIC X. + 20 var6 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 31, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 2, "name": "one-level" + }, + { + "vm": 2, "name": "two-levels" + }, + { + "vm": 2, "name": "three-levels" + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 one-level. + 05 root-table-1-count PIC S9(2) BINARY. + 05 root-table-1 OCCURS 10 + DEPENDING ON root-table-1-count. + 10 var1 PIC X. + 10 var2 PIC X. + 01 two-levels. + 05 root-table-2-count PIC S9(3) BINARY. + 05 table-level1-1-count PIC S9(2) BINARY. + 05 root-table-2 OCCURS 100 + DEPENDING ON root-table-2-count. + 10 table-level1-1 OCCURS 20 + DEPENDING ON table-level1-1-count. + 15 var3 PIC X. + 15 var4 PIC X. + 01 three-levels. + 05 root-table-3-count PIC S9(4) BINARY. + 05 table-level1-2-count PIC S9(3) BINARY. + 05 table-level2-1-count PIC S9(2) BINARY. + 05 root-table-3 OCCURS 1000 + DEPENDING ON root-table-3-count. + 10 table-level1-2 OCCURS 200 + DEPENDING ON table-level1-2-count. + 15 table-level2-1 OCCURS 30 + DEPENDING ON table-level2-1-count. + 20 var5 PIC X. + 20 var6 PIC X. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D77 Idx-d4df4249-1 PIC 9(4) COMP-5. + D77 Idx-d4df4249-2 PIC 9(4) COMP-5. + D77 Idx-d4df4249-3 PIC 9(4) COMP-5. + * + + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY 'one-level' + D DISPLAY ' root-table-1-count <' root-table-1-count '>' + D IF root-table-1-count >= 1 AND <= 10 + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > root-table-1-count + D DISPLAY ' root-table-1 (' Idx-d4df4249-1 ')' + D DISPLAY ' var1 (' Idx-d4df4249-1 ') <' var1 + D (Idx-d4df4249-1) '>' + D DISPLAY ' var2 (' Idx-d4df4249-1 ') <' var2 + D (Idx-d4df4249-1) '>' + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "root-table-1" because its DEPENDI' + D 'NG ON "root-table-1-count" is out of range.' + D END-IF + D DISPLAY 'two-levels' + D DISPLAY ' root-table-2-count <' root-table-2-count '>' + D DISPLAY ' table-level1-1-count <' table-level1-1-count '>' + D IF root-table-2-count >= 1 AND <= 100 + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > root-table-2-count + D DISPLAY ' root-table-2 (' Idx-d4df4249-1 ')' + D IF table-level1-1-count >= 1 AND <= 20 + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > table-level1-1-count + D DISPLAY ' table-level1-1 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ')' + D DISPLAY ' var3 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ') <' var3 (Idx-d4df4249-1 + D Idx-d4df4249-2) '>' + D DISPLAY ' var4 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ') <' var4 (Idx-d4df4249-1 + D Idx-d4df4249-2) '>' + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "table-level1-1" because its D' + D 'EPENDING ON "table-level1-1-count" is out of ' + D 'range.' + D END-IF + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "root-table-2" because its DEPENDI' + D 'NG ON "root-table-2-count" is out of range.' + D END-IF + D DISPLAY 'three-levels' + D DISPLAY ' root-table-3-count <' root-table-3-count '>' + D DISPLAY ' table-level1-2-count <' table-level1-2-count '>' + D DISPLAY ' table-level2-1-count <' table-level2-1-count '>' + D IF root-table-3-count >= 1 AND <= 1000 + D PERFORM VARYING Idx-d4df4249-1 FROM 1 BY 1 UNTIL + D Idx-d4df4249-1 > root-table-3-count + D DISPLAY ' root-table-3 (' Idx-d4df4249-1 ')' + D IF table-level1-2-count >= 1 AND <= 200 + D PERFORM VARYING Idx-d4df4249-2 FROM 1 BY 1 UNTIL + D Idx-d4df4249-2 > table-level1-2-count + D DISPLAY ' table-level1-2 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ')' + D IF table-level2-1-count >= 1 AND <= 30 + D PERFORM VARYING Idx-d4df4249-3 FROM 1 BY 1 UNTIL + D Idx-d4df4249-3 > table-level2-1-count + D DISPLAY ' table-level2-1 (' Idx-d4df4249-1 + D ' ' Idx-d4df4249-2 ' ' Idx-d4df4249-3 ')' + D DISPLAY ' var5 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') <' var5 + D (Idx-d4df4249-1 Idx-d4df4249-2 Idx-d4df4249-3) + D '>' + D DISPLAY ' var6 (' Idx-d4df4249-1 ' ' + D Idx-d4df4249-2 ' ' Idx-d4df4249-3 ') <' var6 + D (Idx-d4df4249-1 Idx-d4df4249-2 Idx-d4df4249-3) + D '>' + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "table-level2-1" because i' + D 'ts DEPENDING ON "table-level2-1-count" is' + D ' out of range.' + D END-IF + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "table-level1-2" because its D' + D 'EPENDING ON "table-level1-2-count" is out of ' + D 'range.' + D END-IF + D END-PERFORM + D ELSE + D DISPLAY 'Cannot DISPLAY "root-table-3" because its DEPENDI' + D 'NG ON "root-table-3-count" is out of range.' + D END-IF + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/Fillers.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/Fillers.cbl new file mode 100644 index 000000000..ffc3204c5 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/Fillers.cbl @@ -0,0 +1,123 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC X. + 10 FILLER PIC X(20). + 10 var2 PIC X. + 01 group2. + 05 group2-1. + 10 var3 PIC X. + 10 PIC X. + 10 var5 PIC X. + 01 group3. + 05 var6 PIC X(10). + 05 . + 10 var7 PIC X(5). + 10 FILLER PIC X(20). + 01 . + 05 . + 10 var8 PIC X. + 10 FILLER PIC X(20). + 10 var9 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 24, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "group1", "ch": [ + { + "vm": 1, "name": "group1-1", "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + }, + { + "vm": 1, "name": "group2", "ch": [ + { + "vm": 1, "name": "group2-1", "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + }, + { + "vm": 1, "name": "group3", "ch": [ + { + "vm": 1, "idx": 1, "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + }, + { + "vm": 1, "idx": 3, "ch": [ + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC X. + 10 FILLER PIC X(20). + 10 var2 PIC X. + 01 group2. + 05 group2-1. + 10 var3 PIC X. + 10 PIC X. + 10 var5 PIC X. + 01 group3. + 05 var6 PIC X(10). + 05 . + 10 var7 PIC X(5). + 10 FILLER PIC X(20). + 01 . + 05 . + 10 var8 PIC X. + 10 FILLER PIC X(20). + 10 var9 PIC X. + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY ' FILLER (2:20) <' group1-1 (2:20) '>' + D DISPLAY ' FILLER (2:1) <' group2-1 (2:1) '>' + D DISPLAY ' FILLER <' group3 (16:20) '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/NoModification.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/NoModification.cbl new file mode 100644 index 000000000..77c782dae --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/NoModification.cbl @@ -0,0 +1,54 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 . + 05 . + 10 var1 PIC X. + 10 FILLER PIC X(20). + 10 var2 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 9, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=No modification. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 . + 05 . + 10 var1 PIC X. + 10 FILLER PIC X(20). + 10 var2 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/OtherDataSections.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/OtherDataSections.cbl new file mode 100644 index 000000000..7b0a45140 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/OtherDataSections.cbl @@ -0,0 +1,54 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var-working PIC X(200). + LOCAL-STORAGE SECTION. + 01 var-local PIC X(200). + LINKAGE SECTION. + 01 var-linkage PIC X(200). + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 9, "character": 26 } + }, + false, + { + "vm": 2, "idx": 0 + }, + { + "vm": 2, "idx": 1 + }, + { + "vm": 2, "idx": 2 + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var-working PIC X(200). + LOCAL-STORAGE SECTION. + 01 var-local PIC X(200). + LINKAGE SECTION. + 01 var-linkage PIC X(200). + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY 'var-working <' var-working '>' + D DISPLAY 'var-local <' var-local '>' + D DISPLAY 'var-linkage <' var-linkage '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/SimpleTest.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/SimpleTest.cbl new file mode 100644 index 000000000..bbbf3c8d9 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/SimpleTest.cbl @@ -0,0 +1,42 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var1 PIC X(200). + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 5, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 0, "name": "var1" + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var1 PIC X(200). + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY 'var1 <' var1 '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/WithGroups.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/WithGroups.cbl new file mode 100644 index 000000000..326abbe1c --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleData/WithGroups.cbl @@ -0,0 +1,102 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC X. + 10 var2 PIC X. + 01 group2. + 05 group2-1. + 10 var3 PIC X. + 10 var4 PIC X. + 10 var5 PIC X. + 05 group2-2. + 10 var6 PIC X. + 10 var7 PIc X. + 01 group3. + 05 group3-1. + 10 var8 PIC X. + 05 group3-2. + 10 var9 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 21, "character": 26 } + }, + false, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "group1", "ch": [ + { + "vm": 1, "name": "group1-1", "ch": [ + { + "vm": 0, "name": "var1" + } + ] + } + ] + }, + { + "vm": 2, "name": "group2" + }, + { + "vm": 0, "name": "group3", "ch": [ + { + "vm": 0, "name": "group3-1" + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC X. + 10 var2 PIC X. + 01 group2. + 05 group2-1. + 10 var3 PIC X. + 10 var4 PIC X. + 10 var5 PIC X. + 05 group2-2. + 10 var6 PIC X. + 10 var7 PIc X. + 01 group3. + 05 group3-1. + 10 var8 PIC X. + 05 group3-2. + 10 var9 PIC X. + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY ' var1 <' var1 '>' + D DISPLAY 'group2' + D DISPLAY ' group2-1' + D DISPLAY ' var3 <' var3 '>' + D DISPLAY ' var4 <' var4 '>' + D DISPLAY ' var5 <' var5 '>' + D DISPLAY ' group2-2' + D DISPLAY ' var6 <' var6 '>' + D DISPLAY ' var7 <' var7 '>' + D DISPLAY 'group3' + D DISPLAY ' group3-1 <' group3-1 '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/Fillers.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/Fillers.cbl new file mode 100644 index 000000000..8533cb911 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/Fillers.cbl @@ -0,0 +1,123 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC X. + 10 FILLER PIC X(20). + 10 var2 PIC X. + 01 group2. + 05 group2-1. + 10 var3 PIC X. + 10 PIC X. + 10 var5 PIC X. + 01 group3. + 05 var6 PIC X(10). + 05 . + 10 var7 PIC X(5). + 10 FILLER PIC X(20). + 01 . + 05 . + 10 var8 PIC X. + 10 FILLER PIC X(20). + 10 var9 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 25, "character": 11 } + }, + true, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "group1", "ch": [ + { + "vm": 1, "name": "group1-1", "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + }, + { + "vm": 1, "name": "group2", "ch": [ + { + "vm": 1, "name": "group2-1", "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + }, + { + "vm": 1, "name": "group3", "ch": [ + { + "vm": 1, "idx": 1, "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + }, + { + "vm": 1, "idx": 3, "ch": [ + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 0, "idx": 1 + } + ] + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC X. + 10 FILLER PIC X(20). + 10 var2 PIC X. + 01 group2. + 05 group2-1. + 10 var3 PIC X. + 10 PIC X. + 10 var5 PIC X. + 01 group3. + 05 var6 PIC X(10). + 05 . + 10 var7 PIC X(5). + 10 FILLER PIC X(20). + 01 . + 05 . + 10 var8 PIC X. + 10 FILLER PIC X(20). + 10 var9 PIC X. + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY ' FILLER (2:20) <' group1-1 (2:20) '>' + D DISPLAY ' FILLER (2:1) <' group2-1 (2:1) '>' + D DISPLAY ' FILLER <' group3 (16:20) '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/OtherDataSections.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/OtherDataSections.cbl new file mode 100644 index 000000000..e3b0869aa --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/OtherDataSections.cbl @@ -0,0 +1,54 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var-working PIC X(200). + LOCAL-STORAGE SECTION. + 01 var-local PIC X(200). + LINKAGE SECTION. + 01 var-linkage PIC X(200). + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 10, "character": 11 } + }, + true, + { + "vm": 2, "idx": 0 + }, + { + "vm": 2, "idx": 1 + }, + { + "vm": 2, "idx": 2 + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var-working PIC X(200). + LOCAL-STORAGE SECTION. + 01 var-local PIC X(200). + LINKAGE SECTION. + 01 var-linkage PIC X(200). + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY 'var-working <' var-working '>' + D DISPLAY 'var-local <' var-local '>' + D DISPLAY 'var-linkage <' var-linkage '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/SimpleTest.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/SimpleTest.cbl new file mode 100644 index 000000000..f0d835dd7 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/SimpleTest.cbl @@ -0,0 +1,42 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var1 PIC X(200). + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 6, "character": 11 } + }, + true, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 0, "name": "var1" + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 var1 PIC X(200). + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY 'var1 <' var1 '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/WithGroups.cbl b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/WithGroups.cbl new file mode 100644 index 000000000..cfb3e1975 --- /dev/null +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/InsertVariableDisplay/SimpleDataInsertBefore/WithGroups.cbl @@ -0,0 +1,102 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC X. + 10 var2 PIC X. + 01 group2. + 05 group2-1. + 10 var3 PIC X. + 10 var4 PIC X. + 10 var5 PIC X. + 05 group2-2. + 10 var6 PIC X. + 10 var7 PIc X. + 01 group3. + 05 group3-1. + 10 var8 PIC X. + 05 group3-2. + 10 var9 PIC X. + PROCEDURE DIVISION. + GOBACK + . + END PROGRAM TCOMFL06. +------------------------------------------------------------------------------------------------- +TypeCobol.LanguageServer.Commands.InsertVariableDisplay.InsertVariableDisplayRefactoringProcessor +------------------------------------------------------------------------------------------------- +[ + { + "textDocument": { "uri": "file:/test.expected.cbl" }, + "position": { "line": 22, "character": 11 } + }, + true, + { + "vm": 1, "idx": 0, "ch": [ + { + "vm": 1, "name": "group1", "ch": [ + { + "vm": 1, "name": "group1-1", "ch": [ + { + "vm": 0, "name": "var1" + } + ] + } + ] + }, + { + "vm": 2, "name": "group2" + }, + { + "vm": 0, "name": "group3", "ch": [ + { + "vm": 0, "name": "group3-1" + } + ] + } + ] + } +] +------------------------------------------------------------------------------------------------- +refactoring.label=Debug instructions successfully generated. +refactoring.source= + IDENTIFICATION DIVISION. + PROGRAM-ID. TCOMFL06. + DATA DIVISION. + WORKING-STORAGE SECTION. + 01 group1. + 05 group1-1. + 10 var1 PIC X. + 10 var2 PIC X. + 01 group2. + 05 group2-1. + 10 var3 PIC X. + 10 var4 PIC X. + 10 var5 PIC X. + 05 group2-2. + 10 var6 PIC X. + 10 var7 PIc X. + 01 group3. + 05 group3-1. + 10 var8 PIC X. + 05 group3-2. + 10 var9 PIC X. + PROCEDURE DIVISION. + *InsertVariableDisplay 1959/09/18 11:09 TESTUSER + D DISPLAY ' var1 <' var1 '>' + D DISPLAY 'group2' + D DISPLAY ' group2-1' + D DISPLAY ' var3 <' var3 '>' + D DISPLAY ' var4 <' var4 '>' + D DISPLAY ' var5 <' var5 '>' + D DISPLAY ' group2-2' + D DISPLAY ' var6 <' var6 '>' + D DISPLAY ' var7 <' var7 '>' + D DISPLAY 'group3' + D DISPLAY ' group3-1 <' group3-1 '>' + * + + GOBACK + . + END PROGRAM TCOMFL06. \ No newline at end of file diff --git a/TypeCobol.LanguageServer.Test/RefactoringTests/RefactoringProcessorTest.cs b/TypeCobol.LanguageServer.Test/RefactoringTests/RefactoringProcessorTest.cs index 4400c21c7..e99f3eb27 100644 --- a/TypeCobol.LanguageServer.Test/RefactoringTests/RefactoringProcessorTest.cs +++ b/TypeCobol.LanguageServer.Test/RefactoringTests/RefactoringProcessorTest.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json.Linq; using TypeCobol.Compiler; using TypeCobol.Compiler.Directives; -using TypeCobol.LanguageServer.Commands.Refactor; +using TypeCobol.LanguageServer.Commands; using TypeCobol.LanguageServer.Test.Utilities; using TypeCobol.LanguageServer.VsCodeProtocol; using TypeCobol.Test; @@ -13,6 +13,24 @@ namespace TypeCobol.LanguageServer.Test.RefactoringTests { internal class RefactoringProcessorTest { + /// + /// Mocks environment variables to get consistent results during unit tests. + /// + private class TestEnvironment : IEnvironmentVariableProvider + + { + public static readonly TestEnvironment Instance = new TestEnvironment(); + + private TestEnvironment() + { + + } + + public DateTime Now => new DateTime(new DateOnly(1959, 9, 18), new TimeOnly(11, 9)); // It's always time to reinvent COBOL when doing unit tests ! + + public string UserName => "TESTUSER"; + } + private static readonly Dictionary _RefactoringProcessors; static RefactoringProcessorTest() @@ -24,10 +42,12 @@ static RefactoringProcessorTest() var assembly = refactoringProcessorType.Assembly; // Consider types in TypeCobol.LanguageServer assembly foreach (var type in assembly.GetTypes()) { - if (type.GetInterfaces().Contains(refactoringProcessorType)) + if (!type.IsAbstract && type.GetInterfaces().Contains(refactoringProcessorType)) { - // Found a type implementing directly IRefactoringProcessor + // Found a concrete type implementing IRefactoringProcessor var instance = (IRefactoringProcessor)Activator.CreateInstance(type); // Expecting a parameterless constructor + Debug.Assert(instance != null); + instance.EnvironmentVariableProvider = TestEnvironment.Instance; // Use controlled environment variables for testing Debug.Assert(type.FullName != null); _RefactoringProcessors.Add(type.FullName, instance); } diff --git a/TypeCobol.LanguageServer.Test/Utilities/LanguageServerTestUtils.cs b/TypeCobol.LanguageServer.Test/Utilities/LanguageServerTestUtils.cs index 318f4bfc3..e5afa4706 100644 --- a/TypeCobol.LanguageServer.Test/Utilities/LanguageServerTestUtils.cs +++ b/TypeCobol.LanguageServer.Test/Utilities/LanguageServerTestUtils.cs @@ -23,7 +23,7 @@ public static List ParseMultiplePartsContent(string filePath) { while (reader.ReadLine() is { } line) // Non-null pattern + variable definition { - if (line.All(c => c == '-')) + if (line.Length > 0 && line.All(c => c == '-')) { // The line is a separator, flush content read so far into a part parts.Add(builder.ToString()[..^Environment.NewLine.Length]); // Remove trailing line break diff --git a/TypeCobol.LanguageServer/Commands/Refactor/AbstractSingleDocumentRefactoring.cs b/TypeCobol.LanguageServer/Commands/AbstractSingleDocumentRefactoring.cs similarity index 98% rename from TypeCobol.LanguageServer/Commands/Refactor/AbstractSingleDocumentRefactoring.cs rename to TypeCobol.LanguageServer/Commands/AbstractSingleDocumentRefactoring.cs index e8d601166..1606cae4a 100644 --- a/TypeCobol.LanguageServer/Commands/Refactor/AbstractSingleDocumentRefactoring.cs +++ b/TypeCobol.LanguageServer/Commands/AbstractSingleDocumentRefactoring.cs @@ -1,7 +1,7 @@ using TypeCobol.Compiler; using TypeCobol.LanguageServer.VsCodeProtocol; -namespace TypeCobol.LanguageServer.Commands.Refactor +namespace TypeCobol.LanguageServer.Commands { /// /// Base class for Cobol document refactorings. This specialization of AbstractCommand diff --git a/TypeCobol.LanguageServer/Commands/Refactor/AdjustFillers.cs b/TypeCobol.LanguageServer/Commands/AdjustFillers/AdjustFillers.cs similarity index 74% rename from TypeCobol.LanguageServer/Commands/Refactor/AdjustFillers.cs rename to TypeCobol.LanguageServer/Commands/AdjustFillers/AdjustFillers.cs index 56b68c33d..a0e6bc8cc 100644 --- a/TypeCobol.LanguageServer/Commands/Refactor/AdjustFillers.cs +++ b/TypeCobol.LanguageServer/Commands/AdjustFillers/AdjustFillers.cs @@ -1,6 +1,6 @@ -namespace TypeCobol.LanguageServer.Commands.Refactor +namespace TypeCobol.LanguageServer.Commands.AdjustFillers { - internal class AdjustFillers : AbstractSingleDocumentRefactoring + internal class AdjustFillers : AbstractSingleDocumentRefactoring { public static AdjustFillers Create(TypeCobolServer typeCobolServer) => new(typeCobolServer); diff --git a/TypeCobol.LanguageServer/Commands/Refactor/AdjustFillerRefactoringProcessor.cs b/TypeCobol.LanguageServer/Commands/AdjustFillers/AdjustFillersRefactoringProcessor.cs similarity index 93% rename from TypeCobol.LanguageServer/Commands/Refactor/AdjustFillerRefactoringProcessor.cs rename to TypeCobol.LanguageServer/Commands/AdjustFillers/AdjustFillersRefactoringProcessor.cs index 42fa4b25b..ef5798e4d 100644 --- a/TypeCobol.LanguageServer/Commands/Refactor/AdjustFillerRefactoringProcessor.cs +++ b/TypeCobol.LanguageServer/Commands/AdjustFillers/AdjustFillersRefactoringProcessor.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Text; -using Newtonsoft.Json.Linq; using TypeCobol.Compiler; using TypeCobol.Compiler.CodeElements; using TypeCobol.Compiler.Nodes; @@ -8,12 +7,12 @@ using TypeCobol.Compiler.Text; using TypeCobol.LanguageServer.VsCodeProtocol; -namespace TypeCobol.LanguageServer.Commands.Refactor +namespace TypeCobol.LanguageServer.Commands.AdjustFillers { /// /// Refactoring processor for AdjustFillers command /// - public class AdjustFillerRefactoringProcessor : IRefactoringProcessor + public class AdjustFillersRefactoringProcessor : AbstractRefactoringProcessor { private class AdjustFillerVisitor : AbstractAstVisitor { @@ -214,18 +213,13 @@ void Remove() } } - public TextDocumentIdentifier PrepareRefactoring(object[] arguments) + public override TextDocumentIdentifier PrepareRefactoring(object[] arguments) { - // TODO How to factorize for all processors ? - if (arguments == null || arguments.Length == 0 || arguments[0] is not JObject jObject) - { - throw new ArgumentException("Invalid arguments for command.", nameof(arguments)); - } - - return jObject.ToObject(); + // Single argument is the text document identifier and it is required + return Expect(arguments, 0, true); } - public void CheckTarget(CompilationUnit compilationUnit) + public override void CheckTarget(CompilationUnit compilationUnit) { // Require full AST if (compilationUnit.ProgramClassDocumentSnapshot == null) @@ -234,7 +228,7 @@ public void CheckTarget(CompilationUnit compilationUnit) } } - public (string Label, List TextEdits) PerformRefactoring(CompilationUnit compilationUnit) + public override (string Label, List TextEdits) PerformRefactoring(CompilationUnit compilationUnit) { // Compute edits using a visitor var visitor = new AdjustFillerVisitor(); diff --git a/TypeCobol.LanguageServer/Commands/ICommand.cs b/TypeCobol.LanguageServer/Commands/ICommand.cs index bbb1038be..10ed5658c 100644 --- a/TypeCobol.LanguageServer/Commands/ICommand.cs +++ b/TypeCobol.LanguageServer/Commands/ICommand.cs @@ -11,7 +11,8 @@ static ICommand() { _Commands = new Dictionary>() { - { "refactor/adjustFillers", Refactor.AdjustFillers.Create } + { "refactor/adjustFillers", AdjustFillers.AdjustFillers.Create }, + { "source/insertVariableDisplay", InsertVariableDisplay.InsertVariableDisplay.Create } // Register all supported commands here }; } diff --git a/TypeCobol.LanguageServer/Commands/IEnvironmentVariableProvider.cs b/TypeCobol.LanguageServer/Commands/IEnvironmentVariableProvider.cs new file mode 100644 index 000000000..eddc1876f --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/IEnvironmentVariableProvider.cs @@ -0,0 +1,30 @@ +namespace TypeCobol.LanguageServer.Commands +{ + /// + /// Abstraction for environment. Override in unit tests to get consistent results + /// across environments. + /// + public interface IEnvironmentVariableProvider + { + DateTime Now { get; } + + string UserName { get; } + } + + /// + /// Real implementation of the environment. + /// + public class RealEnvironment : IEnvironmentVariableProvider + { + public static readonly RealEnvironment Instance = new RealEnvironment(); + + private RealEnvironment() + { + + } + + public DateTime Now => DateTime.Now; + + public string UserName => Environment.UserName; + } +} diff --git a/TypeCobol.LanguageServer/Commands/IRefactoringProcessor.cs b/TypeCobol.LanguageServer/Commands/IRefactoringProcessor.cs new file mode 100644 index 000000000..776a9aceb --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/IRefactoringProcessor.cs @@ -0,0 +1,102 @@ +using Newtonsoft.Json.Linq; +using TypeCobol.Compiler; +using TypeCobol.LanguageServer.VsCodeProtocol; + +namespace TypeCobol.LanguageServer.Commands +{ + /// + /// A refactoring processor is responsible for implementing the refactoring logic + /// independently of the language server currently running. This is an abstraction + /// for an automated modification of source text. + /// + public interface IRefactoringProcessor + { + /// + /// Sets the environment variable provider for this instance. + /// + IEnvironmentVariableProvider EnvironmentVariableProvider { set; } + + /// + /// Collect and parse refactoring arguments. + /// + /// Untyped argument array, possibly null. + /// Non-null TextDocumentIdentifier found in args, an exception should be thrown if no identifier could be found. + TextDocumentIdentifier PrepareRefactoring(object[] arguments); + + /// + /// Perform validation of the refactoring target. + /// + /// The program that should be modified by this refactoring. + void CheckTarget(CompilationUnit compilationUnit); + + /// + /// Compute text edits corresponding to this refactoring. + /// + /// Target of the refactoring. + /// A tuple made of a label describing the modification that has been performed and a list of TextEdits. + (string Label, List TextEdits) PerformRefactoring(CompilationUnit compilationUnit); + } + + /// + /// Base class for refactoring processors. + /// + public abstract class AbstractRefactoringProcessor : IRefactoringProcessor + { + /// + /// Arguments deserialization helper. Returns an object read or parsed from + /// given arguments array and index. + /// + /// Expected type of the argument. + /// Arguments array. + /// Argument index. + /// True to consider the argument as required: when missing, an ArgumentException + /// will be thrown. False to consider the argument as optional: the default value for the argument type + /// will be returned. + /// Instance of T, possibly null. + protected static T Expect(object[] arguments, int index, bool required) + { + if (arguments == null || index > arguments.Length - 1) + { + return DefaultOrThrowWhenRequired(); + } + + object argument = arguments[index]; + switch (argument) + { + case T result: + return result; + case JToken jToken: + if (jToken.Type == JTokenType.Null) + { + ThrowWhenRequired(); + } + + return jToken.ToObject(); + } + + return DefaultOrThrowWhenRequired(); + + T DefaultOrThrowWhenRequired() + { + ThrowWhenRequired(); + return default; + } + + void ThrowWhenRequired() + { + if (required) + { + throw new ArgumentException("Invalid arguments for command.", nameof(arguments)); + } + } + } + + public IEnvironmentVariableProvider EnvironmentVariableProvider { get; set; } = RealEnvironment.Instance; // Initialize with real environment + + public abstract TextDocumentIdentifier PrepareRefactoring(object[] arguments); + + public abstract void CheckTarget(CompilationUnit compilationUnit); + + public abstract (string Label, List TextEdits) PerformRefactoring(CompilationUnit compilationUnit); + } +} diff --git a/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/CobolStringBuilder.cs b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/CobolStringBuilder.cs new file mode 100644 index 000000000..3cb9c846d --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/CobolStringBuilder.cs @@ -0,0 +1,217 @@ +using System.Diagnostics; +using System.Text; +using TypeCobol.Compiler.Text; + +namespace TypeCobol.LanguageServer.Commands.InsertVariableDisplay +{ + /// + /// Imitates StringBuilder but for generating Cobol code according to the RDz Reference Format. + /// Internally it uses 2 StringBuilder: one for the accumulated text so far and one + /// for the current line being built. When the current line exceeds the maximum allowed length, + /// its content is transferred to the text and a new line is initialized. + /// + [DebuggerDisplay("Text = {_text}, CurrentLine = {_currentLine}")] // The ToString() method may alter the object, so we use custom debug display to avoid side effects + internal class CobolStringBuilder + { + private const int MAX_LINE_LENGTH = (int)CobolFormatAreas.End_B; // 72 + private const char ONE_SPACE = ' '; + private const char DEBUG_INDICATOR = 'D'; + private const char COMMENT_INDICATOR = '*'; + + private static readonly string _SequenceNumber = new string(ONE_SPACE, CobolFormatAreas.EndNumber - CobolFormatAreas.BeginNumber + 1); // 6 spaces + + private readonly char _indicator; + private readonly StringBuilder _text; + private readonly StringBuilder _currentLine; + private bool _currentLineIsEmpty; + + public CobolStringBuilder(bool debug = false) + { + _indicator = debug ? DEBUG_INDICATOR : ONE_SPACE; + _text = new StringBuilder(); + _currentLine = new StringBuilder(); + Clear(); + } + + public void Clear() + { + _text.Clear(); + _currentLine.Clear(); + InitCurrentLine(); + } + + private void InitCurrentLine() + { + _currentLine.Append(_SequenceNumber); + _currentLine.Append(_indicator); + _currentLineIsEmpty = true; + } + + private int FlushCurrentLine() + { + string currentLine = _currentLine.ToString(); + + // Number of leading spaces on current line, does not include sequence number and indicator + int previousLineIndentLength = currentLine.Skip(_SequenceNumber.Length + 1).TakeWhile(c => c == ONE_SPACE).Count(); + + _text.Append(currentLine); + _currentLine.Clear(); + return previousLineIndentLength; + } + + /// + /// Append the given number of spaces at the beginning (after indicator) of the current line. + /// This method expects that: + /// - the current line is empty + /// - the number of spaces does not exceed the maximum line length + /// + /// Number os leading spaces to add to current line. + public void AppendIndent(int length) + { + Debug.Assert(_currentLineIsEmpty); + Debug.Assert(_currentLine.Length + length <= MAX_LINE_LENGTH); + string indent = new string(ONE_SPACE, length); + _currentLine.Append(indent); + } + + /// + /// Append the given word to the current line. The word is considered as non-breakable, + /// meaning a new line will be created if the word does not fit on the current line. + /// + /// Cobol text to add on current line. + public void AppendWord(string word) + { + AppendText(word); + _currentLineIsEmpty = false; + } + + private void AppendText(string text) + { + bool addSeparator = !_currentLineIsEmpty; + int separatorLength = addSeparator ? 1 : 0; + if (_currentLine.Length + separatorLength + text.Length > MAX_LINE_LENGTH) + { + // Word is too long to fit on current line, create a new line and auto align + int indent = AppendLine(); + AppendIndent(indent); + } + else if (addSeparator) + { + _currentLine.Append(ONE_SPACE); + } + + _currentLine.Append(text); + } + + /// + /// Append the given literal after a DISPLAY keyword. This is a hack as this method does not + /// handle literal splitting and continuation lines. Instead, the literal is broken at column 72 + /// and the concatenation is done at runtime by the DISPLAY statement itself. + /// + /// Literal value to append, without delimiters. + /// Desired delimiter, by default the method will use a single quote. + public void AppendLiteralForDisplay(string literalValue, char delimiter = '\'') + { + Debug.Assert(!_currentLineIsEmpty); + Debug.Assert(_currentLine.ToString().EndsWith("DISPLAY")); + + bool addSeparator = true; + int indent = _currentLine.Length - _SequenceNumber.Length; // If a new line is required, the text will be aligned with the beginning of the literal + + string text = literalValue; + int remaining; // Remaining available chars on current line + int addedCharsCount; // Number of extra chars required, 2 for the two delimiters or 3 when we also have to add the separator after DISPLAY keyword + while (text.Length + (addedCharsCount = (addSeparator ? 3 : 2)) > (remaining = MAX_LINE_LENGTH - _currentLine.Length)) + { + // Split literal + string part = text.Substring(0, remaining - addedCharsCount); + if (addSeparator) + { + _currentLine.Append(ONE_SPACE); + } + + // Append beginning + _currentLine.Append(delimiter + part + delimiter); + + // Create new line and align + AppendLine(); + AppendIndent(indent); + + // Compute remaining text to append and continue + text = text.Substring(part.Length); + addSeparator = false; + } + + // Append last part(this may be the full literal itself if it did not need to be split) + if (text.Length > 0) + { + if (addSeparator) + { + _currentLine.Append(ONE_SPACE); + } + + _currentLine.Append(delimiter + text + delimiter); + _currentLineIsEmpty = false; + } + } + + /// + /// Break current line and initialize a new one. + /// Return the current line indent length to allow caller to align text with previous line + /// when starting writing on the newly created line. + /// + /// Current line (before break) indent length. + public int AppendLine() + { + int previousLineIndentLength = FlushCurrentLine(); + _text.AppendLine(); + InitCurrentLine(); + return previousLineIndentLength; + } + + /// + /// + /// + /// + /// + public void AppendCommentSingleLine(string comment) + { + comment ??= string.Empty; + + // 65 chars is the maximum single line comment length, this method does not support comment splitting + Debug.Assert(comment.Length <= CobolFormatAreas.End_B - CobolFormatAreas.Begin_A + 1); + + if (_currentLineIsEmpty) + { + // The current line only contains the sequence number and the indicator and optionally some leading spaces + _currentLine.Clear(); + _currentLine.Append(_SequenceNumber); + _currentLine.Append(COMMENT_INDICATOR); + _currentLine.Append(comment); + AppendLine(); + } + else + { + // Cannot append comment on non-empty line. Create a new one and retry. + AppendLine(); + Debug.Assert(_currentLineIsEmpty); + AppendCommentSingleLine(comment); + } + } + + /// + /// Return full text of this builder. + /// Note that the current line is being flushed. + /// + /// Full text of builder. + public override string ToString() + { + if (!_currentLineIsEmpty) + { + FlushCurrentLine(); + } + + return _text.ToString(); + } + } +} diff --git a/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/DataDefinitionHelper.cs b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/DataDefinitionHelper.cs new file mode 100644 index 000000000..cf0581cd0 --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/DataDefinitionHelper.cs @@ -0,0 +1,130 @@ +using TypeCobol.Compiler.Nodes; + +namespace TypeCobol.LanguageServer.Commands.InsertVariableDisplay +{ + /// + /// Helper class to compute an 'accessor' for a given DataDefinition. + /// An accessor captures all needed information to get access to the targeted data at runtime: if the targeted + /// data is named, the accessor is actually the data itself. For a FILLER or anonymous data, the accessor + /// will contain the first named data that can be used to access the targeted data and the associated + /// position and length in order to read the correct memory area. Position and length combined form the + /// reference modifier. + /// + internal static class DataDefinitionHelper + { + /// + /// Private struct used to compute the leftmost character position expression. + /// + /// Delta in bytes of the targeted data from its parent data. + /// Starts at 1 since the position of a data is 1-based in Cobol. + /// List of tuples made of the name of an index and size of an occurence. + /// Each tuple corresponds to an OCCURS dimension that need to be traversed to get to the targeted data, + /// this is used to access data enclosed in anonymous arrays. + private record struct LeftmostCharacterPositionInfo(long DeltaParent, List<(string Index, long OccurenceSize)> EnclosingOccurs) + { + public LeftmostCharacterPositionInfo() + : this(1, new List<(string Index, long OccurenceSize)>()) + { + + } + + public List ToExpression() + { + // Default position: no need to create an expression. The whole data is used directly. + if (DeltaParent == 1 && EnclosingOccurs.Count == 0) + return null; + + // Start with delta parent + List words = [ DeltaParent.ToString() ]; + + if (EnclosingOccurs.Count > 0) + { + // Add offset for each traversed OCCURS dimension + foreach (var enclosingOccurs in EnclosingOccurs) + { + // Shift from the total size of occurrences based on current index value + // + (Idx1 - 1) * + (Idx2 - 1) * + (Idx3 - 1) * ... + words.Add("+"); + words.Add('(' + enclosingOccurs.Index); + words.Add("-"); + words.Add("1)"); + words.Add("*"); + words.Add(enclosingOccurs.OccurenceSize.ToString()); + } + } + + return words; + } + } + + /// + /// Accessor for a DataDefinition. + /// + /// DataDefinition to use to access originally targeted DataDefinition. + /// Number of indices to use on the accessor data. When the target data is nested under + /// anonymous arrays, the data used to access the target is on a different OCCURS dimension. Only some of the first + /// indices are used and this number indicates the count of them. + /// Reference modifier to apply on Data, when null it means no reference modifier is required. + public record struct DataAccessor(DataDefinition Data, int IndicesCount, string[] ReferenceModifier); + + /// + /// Compute accessor for given DataDefinition and indices. + /// + /// Targeted DataDefinition. + /// Indices required to access the targeted data (subscripts) when it is located in one + /// or more nested OCCURS. + /// + public static DataAccessor GetAccessor(DataDefinition dataDefinition, string[] indices) + { + var info = new LeftmostCharacterPositionInfo(); + + // Find the closest named data + var currentDefinition = dataDefinition; + var parentDefinition = currentDefinition.Parent as DataDefinition; + int index = indices.Length - 1; // Current index used, starting from innermost as we are walking up the chain of parents + while (parentDefinition != null && string.IsNullOrEmpty(currentDefinition.Name)) + { + info.DeltaParent += currentDefinition.StartPosition - parentDefinition.StartPosition; // Update delta + if (currentDefinition.IsTableOccurence) + { + // Capture enclosing OCCURS details + long occurenceSize = currentDefinition.PhysicalLength / currentDefinition.MaxOccurencesCount; + string indexName = indices[index]; + info.EnclosingOccurs.Add((indexName, occurenceSize)); + index--; + } + + currentDefinition = parentDefinition; + parentDefinition = currentDefinition.Parent as DataDefinition; + } + + if (string.IsNullOrEmpty(currentDefinition.Name)) + { + // Could not find any named data: no accessor is available for the given DataDefinition + return new DataAccessor(null, 0, null); + } + + // Compute reference modifier + var leftMostCharacterPositionExpression = info.ToExpression(); + string[] referenceModifier = null; + if (leftMostCharacterPositionExpression != null) + { + referenceModifier = new string[leftMostCharacterPositionExpression.Count]; + for (int i = 0; i < leftMostCharacterPositionExpression.Count; i++) + { + string part = leftMostCharacterPositionExpression[i]; + // Add opening parenthesis on first part + bool isFirst = i == 0; + if (isFirst) part = '(' + part; + // Add length and closing parenthesis on last part + bool isLast = i == leftMostCharacterPositionExpression.Count - 1; + if (isLast) part = part + ':' + dataDefinition.PhysicalLength + ')'; + referenceModifier[i] = part; + } + } + + int indicesCount = index + 1; // Number of indices used to access the data + return new DataAccessor(currentDefinition, indicesCount, referenceModifier); + } + } +} diff --git a/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/DataDefinitionToDisplayVisitor.cs b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/DataDefinitionToDisplayVisitor.cs new file mode 100644 index 000000000..3ccf2f52e --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/DataDefinitionToDisplayVisitor.cs @@ -0,0 +1,137 @@ +using TypeCobol.Compiler.Nodes; + +namespace TypeCobol.LanguageServer.Commands.InsertVariableDisplay +{ + /// + /// Extends SelectedNodeVisitor to build a series of DISPLAY statements, + /// one for each selected node. + /// + internal class DataDefinitionToDisplayVisitor : SelectedNodeVisitor + { + private readonly IndexGenerator _indexGenerator; + private int _dataLogicalLevel; + private readonly Stack _indices; + private GeneratedStatement _currentStatement; + + public DataDefinitionToDisplayVisitor(Selection rootSelection, IndexGenerator indexGenerator) + : base(rootSelection) + { + _indexGenerator = indexGenerator; + _dataLogicalLevel = -1; // Starts at -1 so the 01 levels have a logical level of 0 after being entered + _indices = new Stack(); + GeneratedStatements = new GeneratedRoot(); + _currentStatement = GeneratedStatements; + } + + public GeneratedRoot GeneratedStatements { get; } + + protected override IEnumerable SelectChildren(Node parent) + { + // Auto select mode: all data descriptions, except anonymous + foreach (var child in parent.Children.OfType()) + { + if (string.IsNullOrEmpty(child.Name)) continue; + yield return child; + } + } + + protected override void EnterNode(Node node) + { + if (node is DataDefinition dataDefinition) + { + EnterDataDefinition(dataDefinition); + } + } + + protected override void ProcessNode(Node node, bool nodeHasSelectedChildren) + { + if (node is DataDefinition dataDefinition) + { + ProcessDataDefinition(dataDefinition, nodeHasSelectedChildren); + } + } + + protected override void ExitNode(Node node) + { + if (node is DataDefinition dataDefinition) + { + ExitDataDefinition(dataDefinition); + } + } + + private void EnterDataDefinition(DataDefinition dataDefinition) + { + _dataLogicalLevel++; + + // Generate PERFORM if need be + if (dataDefinition.IsTableOccurence) + { + // Start with MaxOccurs as max and check whether a DEPENDING ON is declared or not + string max = dataDefinition.MaxOccurencesCount.ToString(); + if (dataDefinition.OccursDependingOn != null) + { + // Generate IF IS NUMERIC AND IN RANGE. The DEPENDING ON variable becomes the max for the PERFORM + string maxOccurs = max; + max = dataDefinition.OccursDependingOn.MainSymbolReference.Name; + + // Check DEPENDING ON type: we should not check class when the DEPENDING ON is binary + var dependingOn = dataDefinition.GetDataDefinitionFromStorageAreaDictionary(dataDefinition.OccursDependingOn.StorageArea, true); + var dependingOnUsage = dependingOn?.SemanticData?.Type?.Usage; + bool checkNumeric = dependingOnUsage != Compiler.Types.Type.UsageFormat.Comp && dependingOnUsage != Compiler.Types.Type.UsageFormat.Comp5; + string errorMessage = $"Cannot DISPLAY \"{dataDefinition.Name}\" because its DEPENDING ON \"{max}\" is{(checkNumeric ? " either not numeric or " : " ")}out of range."; + + // Create IF statement and continue generation inside it + var ifNumericAndInRange = new GeneratedIfIsNumericAndInRangeElseDisplayMessage(max, checkNumeric, maxOccurs, errorMessage); + _currentStatement.AddChild(ifNumericAndInRange); + _currentStatement = ifNumericAndInRange; + } + + // Reuse or generate new index for the current OCCURS dimension + string index = _indexGenerator.GetOrCreateIndex(_indices.Count + 1); + _indices.Push(index); + + // Generate PERFORM and continue generation inside it + var perform = new GeneratedPerform(index, max); + _currentStatement.AddChild(perform); + _currentStatement = perform; + } + } + + private void ProcessDataDefinition(DataDefinition dataDefinition, bool dataDefinitionHasSelectedChildren) + { + var indices = _indices.Reverse().ToArray(); + var accessor = DataDefinitionHelper.GetAccessor(dataDefinition, indices); + if (accessor.Data == null) + { + // Cannot generate DISPLAY statement + return; + } + + // Generate DISPLAY + bool withValue = !(dataDefinition.HasChildrenThatDeclareData() && dataDefinitionHasSelectedChildren); + var display = new GeneratedDisplayVariable(_dataLogicalLevel, dataDefinition, accessor, indices, withValue); + _currentStatement.AddChild(display); + } + + private void ExitDataDefinition(DataDefinition dataDefinition) + { + // Mirror the EnterDataDefinition method + if (dataDefinition.IsTableOccurence) + { + // Exit PERFORM + _currentStatement = _currentStatement.Parent; + + if (dataDefinition.OccursDependingOn != null) + { + // Exit IF IS NUMERIC + _currentStatement = _currentStatement.Parent; + } + + // Pop innermost index as we have exited the PERFORM + _indices.Pop(); + } + + _dataLogicalLevel--; + } + } +} diff --git a/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/GeneratedStatements.cs b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/GeneratedStatements.cs new file mode 100644 index 000000000..0c75b0524 --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/GeneratedStatements.cs @@ -0,0 +1,500 @@ +using System.Diagnostics; +using System.Text; +using TypeCobol.Compiler.Nodes; +using TypeCobol.Compiler.Text; + +namespace TypeCobol.LanguageServer.Commands.InsertVariableDisplay +{ + /// + /// Base class for a generated statement. + /// Generated statement, like actual statements, are organized in a tree structure. + /// Some statements are considered inactive unless they get at least one active children + /// statement. For example PERFORM statement is useless until some statements are added + /// into its loop body. + /// + internal abstract class GeneratedStatement + { + protected GeneratedStatement(bool isActive) + { + IsActive = isActive; + Parent = null; + Children = new List(); + } + + /// + /// Indicate whether the statement is active or not, an inactive statement should not + /// generate any code whereas an active statement is meant to be translated to Cobol code. + /// + protected bool IsActive { get; private set; } + + public GeneratedStatement Parent { get; private set; } + + public List Children { get; } + + private void Activate() + { + // Activate this statement and propagate to parent. + IsActive = true; + Parent?.Activate(); + } + + public void AddChild(GeneratedStatement childStatement) + { + childStatement.Parent = this; + Children.Add(childStatement); + if (childStatement.IsActive) + { + Activate(); + } + } + + protected internal void WriteCobolCode(int statementLevel, CobolStringBuilder builder) + { + if (!IsActive) return; // Nothing to generate + + /* + * Generic Cobol code generation algorithm: parameter statementLevel is used to indent statement code + * according to its nesting level and then passed recursively to children statement. + * Every statement has an opening line. For composite statements, the WriteStatementEnd is called to + * get the closing line. + */ + + WriteIndent(); + WriteStatementOpening(builder); + builder.AppendLine(); + + if (Children.Count > 0) + { + foreach (var child in Children) + { + child.WriteCobolCode(statementLevel + 1, builder); + } + + WriteIndent(); + WriteStatementEnd(builder); + builder.AppendLine(); + } + + void WriteIndent() + { + Debug.Assert(statementLevel >= 0); + + int length = CobolFormatAreas.End_A - CobolFormatAreas.Begin_A + 1; // 4 spaces to start in AreaB + length += 2 * statementLevel; // 2 additional spaces for each level of nested statements + builder.AppendIndent(length); + } + } + + /// + /// Derived classes must implement this method to write their own keywords + /// and expressions to the supplied builder. + /// + /// Target builder. + protected abstract void WriteStatementOpening(CobolStringBuilder builder); + + /// + /// For composite statements only, derived classes must implement this method + /// to write their end element. + /// + /// Target builder. + protected abstract void WriteStatementEnd(CobolStringBuilder builder); + } + + /// + /// Root for a tree of generated statements. This does not correspond to any code, + /// however the code for nested statements can be retrieved only using this class. + /// + internal class GeneratedRoot : GeneratedStatement, ICobolCodeProvider + { + public GeneratedRoot() + : base(false) // Initially inactive as it serves no purpose until an active nested statement is added + { + + } + + public bool HasContent => IsActive; + + public void WriteCobolCode(CobolStringBuilder builder) + { + // Write all children + foreach (var child in Children) + { + child.WriteCobolCode(0, builder); + } + } + + protected override void WriteStatementOpening(CobolStringBuilder builder) + { + throw new InvalidOperationException("Root statement has no beginning code element."); + } + + protected override void WriteStatementEnd(CobolStringBuilder builder) + { + throw new InvalidOperationException("Root statement has no ending code element."); + } + } + + /// + /// Class to write a PERFORM statement. + /// Form is: + /// PERFORM VARYING [Index] FROM 1 BY 1 UNTIL [Index] > [Max] + /// [Children] + /// END-PERFORM + /// + internal class GeneratedPerform : GeneratedStatement + { + public string Index { get; } + + public string Max { get; } + + public GeneratedPerform(string index, string max) + : base(false) // Initially inactive as it serves no purpose until an active nested statement is added + { + Index = index; + Max = max; + } + + protected override void WriteStatementOpening(CobolStringBuilder builder) + { + builder.AppendWord("PERFORM"); + builder.AppendWord("VARYING"); + builder.AppendWord(Index); + builder.AppendWord("FROM"); + builder.AppendWord("1"); + builder.AppendWord("BY"); + builder.AppendWord("1"); + builder.AppendWord("UNTIL"); + builder.AppendWord(Index); + builder.AppendWord(">"); + builder.AppendWord(Max); + } + + protected override void WriteStatementEnd(CobolStringBuilder builder) + { + builder.AppendWord("END-PERFORM"); + } + } + + /// + /// Class to write a DISPLAY statement for a variable. + /// Form varies according to the presence or absence of value, indices, logical level + /// of the variable, etc. + /// + internal class GeneratedDisplayVariable : GeneratedStatement + { + /// + /// Define indentation level of the variable name, to replicate its nesting + /// in the output. + /// + public int LogicalLevel { get; } + + /// + /// Data definition to display + /// + public DataDefinition Target { get; } + + /// + /// Accessor for the targeted data + /// + public DataDefinitionHelper.DataAccessor Accessor { get; } + + /// + /// Table of indices to use to access the targeted data, when it is defined + /// under one or more OCCURS. + /// + public string[] Indices { get; } + + /// + /// True to display the value of the variable, False to display its name only. + /// + public bool WithValue { get; } + + public GeneratedDisplayVariable(int logicalLevel, DataDefinition target, DataDefinitionHelper.DataAccessor accessor, string[] indices, bool withValue) + : base(true) // Always active + { + LogicalLevel = logicalLevel; + Target = target; + Accessor = accessor; + Indices = indices; + WithValue = withValue; + } + + protected override void WriteStatementOpening(CobolStringBuilder builder) + { + builder.AppendWord("DISPLAY"); + + var wordBuilder = new StringBuilder(); + bool withIndices = Indices.Length > 0; + switch (withIndices, WithValue) + { + case (false, false): + // No indices, no value: DISPLAY ' var1' + AppendDisplayName(null); + break; + case (false, true): + // No indices but a value: DISPLAY ' var1 <' var1 '>' + AppendDisplayName('<'); + AppendValue(); + AppendClosingValueDelimiter(); + break; + case (true, false): + // Indices but no value: DISPLAY ' var1 (' Idx-1 ' ' Idx-2 ')' + AppendDisplayName('('); + AppendIndicesForDisplay(); + AppendClosingIndicesDelimiterAndReferenceModifierForDisplay(); + break; + case (true, true): + // Indices and value: DISPLAY ' var1 (' Idx-1 ' ' Idx-2 ') <' var1 (Idx-1 Idx-2) '>' + AppendDisplayName('('); + AppendIndicesForDisplay(); + AppendClosingIndicesDelimiterAndReferenceModifierForDisplay(); + AppendValue(); + AppendClosingValueDelimiter(); + break; + } + + void AppendDisplayName(char? openingChar) + { + /* + * DisplayName is a literal describing what is displayed in the output: + * + * Form is: + * openingDelimiter Indent Name referenceModifierPrecededBySpace? openingCharPrecededBySpace? closingDelimiter + * - openingDelimiter and closingDelimiter are the ' (single quote) character + * - Indent is a string made of spaces: 2 spaces for each level of nesting of the targeted data (01 levels start at 0) + * - Name is the name of the targeted data or 'FILLER' for anonymous or FILLERs + * - referenceModifierPrecededBySpace is an optional reference modifier (for anonymous and FILLERs) + * - openingCharPrecededBySpace is depending on what is expected after: + * - null when no indices nor value come after + * - ( when indices come after + * - < when no indices come after and a value is expected + */ + + string indent = new string(' ', 2 * LogicalLevel); + wordBuilder.Append(indent); + + string name = string.IsNullOrEmpty(Target.Name) ? "FILLER" : Target.Name; + wordBuilder.Append(name); + + if (Indices.Length == 0) + { + AppendReferenceModifierForDisplay(); + } + // else: reference modifier will be added after indices in AppendClosingIndicesDelimiterAndReferenceModifierForDisplay + + if (openingChar.HasValue) + { + wordBuilder.Append(' '); + wordBuilder.Append(openingChar.Value); + } + + builder.AppendLiteralForDisplay(wordBuilder.ToString()); + wordBuilder.Clear(); + } + + void AppendValue() + { + if (!string.IsNullOrEmpty(Target.Name) && Target.IsNationalOrNationalEdited()) + { + Debug.Assert(Target == Accessor.Data); // Target is named + + // Display of national items require the use of DISPLAY-OF built-in function + // Note that display of national fillers is not supported by Cobol as the DISPLAY-OF + // function does not accept modified references + builder.AppendWord("FUNCTION"); + builder.AppendWord("DISPLAY-OF"); + + // Append name + wordBuilder.Append('('); + wordBuilder.Append(Target.Name); + if (Indices.Length == 0) + { + wordBuilder.Append(')'); + } + builder.AppendWord(wordBuilder.ToString()); + wordBuilder.Clear(); + + // Append indices (if any) and the closing parenthesis of the DISPLAY-OF call + AppendIndicesForAccess(true); + } + else + { + // Not a National, use accessor info + builder.AppendWord(Accessor.Data.Name); + AppendIndicesForAccess(false); + if (Accessor.ReferenceModifier != null) + { + foreach (var referenceModifierWord in Accessor.ReferenceModifier) + { + builder.AppendWord(referenceModifierWord); + } + } + } + + void AppendIndicesForAccess(bool addClosingParenthesis) + { + // Append (Idx1 Idx2 [...] Idxn) and an additional closing parenthesis when requested + + for (int i = 0; i < Accessor.IndicesCount; i++) + { + bool isFirst = i == 0; + if (isFirst) + { + wordBuilder.Append('('); + } + + wordBuilder.Append(Indices[i]); + + bool isLast = i == Accessor.IndicesCount - 1; + if (isLast) + { + wordBuilder.Append(')'); + if (addClosingParenthesis) + { + wordBuilder.Append(')'); + } + } + + builder.AppendWord(wordBuilder.ToString()); + wordBuilder.Clear(); + } + } + } + + void AppendClosingValueDelimiter() => builder.AppendWord("'>'"); + + void AppendIndicesForDisplay() + { + // Indices for display are separated by a space literal + for (int i = 0; i < Indices.Length; i++) + { + builder.AppendWord(Indices[i]); + + bool isLast = i == Indices.Length - 1; + if (!isLast) + { + builder.AppendWord("' '"); + } + } + } + + void AppendClosingIndicesDelimiterAndReferenceModifierForDisplay() + { + wordBuilder.Append("')"); + + Debug.Assert(Indices.Length > 0); + AppendReferenceModifierForDisplay(); + + if (WithValue) + { + // For opening value + wordBuilder.Append(" <"); + } + + wordBuilder.Append('\''); + builder.AppendWord(wordBuilder.ToString()); + wordBuilder.Clear(); + } + + void AppendReferenceModifierForDisplay() + { + // Do not attempt to represent reference modifier when it is not a direct access (access using the direct parent) + if (Accessor.ReferenceModifier != null && Accessor.Data == Target.Parent) + { + Debug.Assert(Accessor.ReferenceModifier.Length == 1); + wordBuilder.Append(' '); + wordBuilder.Append(Accessor.ReferenceModifier[0]); + } + } + } + + protected override void WriteStatementEnd(CobolStringBuilder builder) + { + throw new InvalidOperationException("DISPLAY statement is not composite."); + } + } + + /// + /// Class to write an IF statement. + /// Form is: + /// IF [Variable] IS NUMERIC AND [Variable] >=1 AND <= [MaxOccurs] + /// [Children] + /// ELSE + /// DISPLAY [ErrorMessage] + /// END-IF + /// + /// or + /// + /// IF [Variable] >=1 AND <= [MaxOccurs] + /// [Children] + /// ELSE + /// DISPLAY [ErrorMessage] + /// END-IF + /// (when the variable cannot be class tested) + /// + internal class GeneratedIfIsNumericAndInRangeElseDisplayMessage : GeneratedStatement + { + public string Variable { get; } + + public bool CheckNumeric { get; } + + public string MaxOccurs { get; } + + public string ErrorMessage { get; } + + public GeneratedIfIsNumericAndInRangeElseDisplayMessage(string variable, bool checkNumeric, string maxOccurs, string errorMessage) + : base(false) // Initially inactive as it serves no purpose until an active nested statement is added + { + Variable = variable; + CheckNumeric = checkNumeric; + MaxOccurs = maxOccurs; + ErrorMessage = errorMessage; + } + + protected override void WriteStatementOpening(CobolStringBuilder builder) + { + builder.AppendWord("IF"); + + if (CheckNumeric) + { + builder.AppendWord(Variable); + builder.AppendWord("IS"); + builder.AppendWord("NUMERIC"); + builder.AppendWord("AND"); + } + + builder.AppendWord(Variable); + builder.AppendWord(">="); + builder.AppendWord("1"); + builder.AppendWord("AND"); + builder.AppendWord("<="); + builder.AppendWord(MaxOccurs); + } + + protected override void WriteStatementEnd(CobolStringBuilder builder) + { + /* + * HACK ! To model properly an IF statement, we should have something like + * IF + * THEN + * [Children of THEN when the condition is true] + * ELSE + * [Children of ELSE when the condition is false] + * END-IF + * + * With THEN a pseudo statement as it should not produce any code and not increase statement level + * + * Instead we hack the WriteStatementEnd to write directly the ELSE and DISPLAY [ErrorMessage] part + * directly without using the real GeneratedDisplay class !!! + */ + builder.AppendWord("ELSE"); + int indent = builder.AppendLine(); + builder.AppendIndent(indent + 2); + builder.AppendWord("DISPLAY"); + builder.AppendLiteralForDisplay(ErrorMessage); + builder.AppendLine(); + builder.AppendIndent(indent); + builder.AppendWord("END-IF"); + } + } +} diff --git a/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/ICobolCodeProvider.cs b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/ICobolCodeProvider.cs new file mode 100644 index 000000000..24b835940 --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/ICobolCodeProvider.cs @@ -0,0 +1,19 @@ +namespace TypeCobol.LanguageServer.Commands.InsertVariableDisplay +{ + /// + /// Abstraction for objects that generate Cobol code. + /// + internal interface ICobolCodeProvider + { + /// + /// True when this provider has content to write, False when empty. + /// + bool HasContent { get; } + + /// + /// Write the entirety of generated code to the given builder. + /// + /// Non-null CobolStringBuilder instance. + void WriteCobolCode(CobolStringBuilder cobolStringBuilder); + } +} diff --git a/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/IndexGenerator.cs b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/IndexGenerator.cs new file mode 100644 index 000000000..d4fb80429 --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/IndexGenerator.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; + +namespace TypeCobol.LanguageServer.Commands.InsertVariableDisplay +{ + /// + /// Generate index variables required when iterating arrays. + /// + internal class IndexGenerator : ICobolCodeProvider + { + private readonly string _hash; + private readonly List _indices; + + public IndexGenerator(string hash) + { + _hash = hash; + _indices = new List(); + } + + public bool HasContent => _indices.Count > 0; + + /// + /// Get the index name for the given OCCURS dimension. + /// If no index exists yet for this dimension, a new index is created. + /// + /// OCCURS dimension, starts at 1. + /// Name of the index to use for this dimension. + public string GetOrCreateIndex(int occursDimension) + { + Debug.Assert(occursDimension >= 1); + + string index; + if (occursDimension > _indices.Count) + { + // Create new index + Debug.Assert(occursDimension == _indices.Count + 1); // Check no gap + index = $"Idx-{_hash}-{occursDimension}"; + _indices.Add(index); + } + else + { + // Reuse index + index = _indices[occursDimension - 1]; + } + + return index; + } + + public void WriteCobolCode(CobolStringBuilder builder) + { + // Each index is generated as a 77 level, PIC 9(4) COMP-5. This allows to iterate arrays having less than 65535 items. + foreach (var index in _indices) + { + // As index name is no longer than 15 chars, the whole declaration fit on one line + builder.AppendWord($"77 {index} PIC 9(4) COMP-5."); + builder.AppendLine(); + } + } + } +} diff --git a/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/InsertVariableDisplay.cs b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/InsertVariableDisplay.cs new file mode 100644 index 000000000..5dfbe1983 --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/InsertVariableDisplay.cs @@ -0,0 +1,13 @@ +namespace TypeCobol.LanguageServer.Commands.InsertVariableDisplay +{ + internal class InsertVariableDisplay : AbstractSingleDocumentRefactoring + { + public static InsertVariableDisplay Create(TypeCobolServer typeCobolServer) => new(typeCobolServer); + + public InsertVariableDisplay(TypeCobolServer typeCobolServer) + : base(typeCobolServer) + { + + } + } +} diff --git a/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/InsertVariableDisplayRefactoringProcessor.cs b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/InsertVariableDisplayRefactoringProcessor.cs new file mode 100644 index 000000000..971009163 --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/InsertVariableDisplay/InsertVariableDisplayRefactoringProcessor.cs @@ -0,0 +1,165 @@ +using System.Diagnostics; +using TypeCobol.Compiler; +using TypeCobol.Compiler.CodeElements; +using TypeCobol.Compiler.Nodes; +using TypeCobol.LanguageServer.Utilities; +using TypeCobol.LanguageServer.VsCodeProtocol; + +namespace TypeCobol.LanguageServer.Commands.InsertVariableDisplay +{ + public class InsertVariableDisplayRefactoringProcessor : AbstractRefactoringProcessor + { + // From refactoring args + private Position _insertAt; + private bool _insertBeforeStatement; + private Selection _workingStorageSectionSelection; + private Selection _localStorageSectionSelection; + private Selection _linkageSectionSelection; + + // Generated using user and timestamp + private string _hash; + + // Computed when checking target program + private (CodeElement CodeElement, Node Node) _location; + + public override TextDocumentIdentifier PrepareRefactoring(object[] arguments) + { + // Get TextDocumentPosition (contains TextDocumentIdentifier and insertion position) + var textDocumentPosition = Expect(arguments, 0, true); + _insertAt = textDocumentPosition.position; + + // Get insert before/after flag + _insertBeforeStatement = Expect(arguments, 1, true); + + // Get Selection objects + _workingStorageSectionSelection = GetSelection(2); + _localStorageSectionSelection = GetSelection(3); + _linkageSectionSelection = GetSelection(4); + + // Generate new hash for this refactoring + _hash = Tools.Hash.CreateCOBOLNameHash($"{EnvironmentVariableProvider.UserName}@{EnvironmentVariableProvider.Now:yyyy/MM/dd HH:mm:ss.fff}"); + + return textDocumentPosition.textDocument; + + Selection GetSelection(int index) + { + var jsonSelection = Expect(arguments, index, false); + return jsonSelection?.Convert(); + } + } + + public override void CheckTarget(CompilationUnit compilationUnit) + { + // Require full AST + if (compilationUnit.ProgramClassDocumentSnapshot == null) + { + throw new InvalidOperationException($"Could not get AST for program '{compilationUnit.TextSourceInfo.Name}'."); + } + + // Check valid insertion location + _location = CodeElementLocator.FindCodeElementAt(compilationUnit, _insertAt); + if (_location.CodeElement == null) + { + throw new InvalidOperationException("Unable to locate program to modify."); + } + } + + public override (string Label, List TextEdits) PerformRefactoring(CompilationUnit compilationUnit) + { + // Get program DATA DIVISION + var program = _location.Node.GetProgramNode(); + var dataDivision = program.Children.OfType().SingleOrDefault(); + var textEdits = new List(); + if (dataDivision != null) + { + // Generate DISPLAY statements for each data section. Required indices are tracked by IndexGenerator, shared between generations. + var indexGenerator = new IndexGenerator(_hash); + var statementsForWorkingStorageVariables = GenerateDisplayStatements(dataDivision.WorkingStorageSection, _workingStorageSectionSelection, indexGenerator); + var statementsForLocalStorageVariables = GenerateDisplayStatements(dataDivision.LocalStorageSection, _localStorageSectionSelection, indexGenerator); + var statementsForLinkageVariables = GenerateDisplayStatements(dataDivision.LinkageSection, _linkageSectionSelection, indexGenerator); + + // The statements and the indices are surrounded by standardized comment + string openingComment = $"{nameof(InsertVariableDisplay)} {EnvironmentVariableProvider.Now:yyyy/MM/dd HH:mm} {EnvironmentVariableProvider.UserName}"; + const string closingComment = ""; + + // Get COBOL code for required indices + var cobolStringBuilder = new CobolStringBuilder(true); + cobolStringBuilder.AppendCommentSingleLine(openingComment); + indexGenerator.WriteCobolCode(cobolStringBuilder); + cobolStringBuilder.AppendCommentSingleLine(closingComment); + bool hasCodeForIndices = indexGenerator.HasContent; + string cobolStringForIndices = cobolStringBuilder.ToString(); + + // Reset builder and get COBOL code for DISPLAY statements (from all 3 sections) + cobolStringBuilder.Clear(); + cobolStringBuilder.AppendCommentSingleLine(openingComment); + statementsForWorkingStorageVariables.WriteCobolCode(cobolStringBuilder); + statementsForLocalStorageVariables.WriteCobolCode(cobolStringBuilder); + statementsForLinkageVariables.WriteCobolCode(cobolStringBuilder); + cobolStringBuilder.AppendCommentSingleLine(closingComment); + bool hasCodeForStatements = statementsForWorkingStorageVariables.HasContent || statementsForLocalStorageVariables.HasContent || statementsForLinkageVariables.HasContent; + string cobolStringForStatements = cobolStringBuilder.ToString(); + + if (hasCodeForIndices) + { + // Insert COBOL code for indices + var targetSection = (DataSection)dataDivision.WorkingStorageSection ?? dataDivision.LocalStorageSection; + if (targetSection == null) + { + // TODO Either move this into CheckTarget and turn it into a global refactoring requirement or generate a WORKING STORAGE ourselves ? + return ("Unable to generate indices: neither WORKING-STORAGE SECTION nor LOCAL-STORAGE SECTION could be found to add generated indices.", new List()); + } + + textEdits.Add(InsertAtEnd(targetSection, cobolStringForIndices)); + } + + if (hasCodeForStatements) + { + // Insert COBOL code for statements + if (_insertBeforeStatement) + { + textEdits.Add(InsertBefore(_location.Node, cobolStringForStatements)); + } + else + { + textEdits.Add(InsertAfter(_location.Node, cobolStringForStatements)); + } + } + } + + return (textEdits.Count > 0 ? "Debug instructions successfully generated." : "No modification.", textEdits); + } + + private static GeneratedRoot GenerateDisplayStatements(DataSection dataSection, Selection rootSelection, IndexGenerator indexGenerator) + { + if (dataSection == null || rootSelection == null) + { + // Nothing to generate + return new GeneratedRoot(); + } + + // Create new visitor and visit the whole data section using the corresponding selection + var visitor = new DataDefinitionToDisplayVisitor(rootSelection, indexGenerator); + visitor.Visit(dataSection); + return visitor.GeneratedStatements; + } + + private static TextEdit InsertBefore(Node node, string code) + { + Debug.Assert(node.CodeElement != null); + int line = node.CodeElement.Line; + int character = 0; + return TextEdit.Insert(new Position() { line = line, character = character }, code + Environment.NewLine); + } + + private static TextEdit InsertAfter(Node node, string code) + { + Debug.Assert(node.CodeElement != null); + int line = node.CodeElement.LineEnd; + int character = node.CodeElement.StopIndex + 1; + return TextEdit.Insert(new Position() { line = line, character = character }, Environment.NewLine + code); + } + + private static TextEdit InsertAtEnd(Node node, string code) => InsertAfter(node.GetLastNode(), code); + } +} diff --git a/TypeCobol.LanguageServer/Commands/Refactor/IRefactoringProcessor.cs b/TypeCobol.LanguageServer/Commands/Refactor/IRefactoringProcessor.cs deleted file mode 100644 index a66a08645..000000000 --- a/TypeCobol.LanguageServer/Commands/Refactor/IRefactoringProcessor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using TypeCobol.Compiler; -using TypeCobol.LanguageServer.VsCodeProtocol; - -namespace TypeCobol.LanguageServer.Commands.Refactor -{ - /// - /// A refactoring processor is responsible for implementing the refactoring logic - /// independently of the language server currently running. This is an abstraction - /// for an automated modification of source text. - /// - public interface IRefactoringProcessor - { - /// - /// Collect and parse refactoring arguments. - /// - /// Untyped argument array, possibly null. - /// Non-null TextDocumentIdentifier found in args, an exception should be thrown if no identifier could be found. - TextDocumentIdentifier PrepareRefactoring(object[] arguments); - - /// - /// Perform validation of the refactoring target. - /// - /// The program that should be modified by this refactoring. - void CheckTarget(CompilationUnit compilationUnit); - - /// - /// Compute text edits corresponding to this refactoring. - /// - /// Target of the refactoring. - /// A tuple made of a label describing the modification that has been performed and a list of TextEdits. - (string Label, List TextEdits) PerformRefactoring(CompilationUnit compilationUnit); - } -} diff --git a/TypeCobol.LanguageServer/Commands/SelectedNodesVisitor.cs b/TypeCobol.LanguageServer/Commands/SelectedNodesVisitor.cs new file mode 100644 index 000000000..27974df79 --- /dev/null +++ b/TypeCobol.LanguageServer/Commands/SelectedNodesVisitor.cs @@ -0,0 +1,275 @@ +using Newtonsoft.Json; +using TypeCobol.Compiler.Nodes; + +namespace TypeCobol.LanguageServer.Commands +{ + /// + /// Serialization class for Selection and its derived types. + /// To avoid type management in JSON string, all properties from all classes are defined + /// in this surrogate class. The client is responsible for sending consistent sets of + /// properties allowing the server to decide which concrete class to instantiate. + /// + public class JsonSelection + { + [JsonProperty("vm")] + public NodeVisitMode VisitMode { get; set; } + + [JsonProperty("ch")] + public JsonSelection[] SubSelections { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("idx")] + public int? Index { get; set; } + + /// + /// Instantiate actual Selection from transferred data. + /// + /// Non-null instance of Selection based on criteria set in this instance. + /// When no valid criteria combination could be found to select the actual Selection type. + internal Selection Convert() + { + Selection instance = null; + + if (Name != null) + { + // Prioritize selection by name + instance = new SelectionByName(Name, VisitMode); + } + else if (Index.HasValue) + { + // Then by index + instance = new SelectionByIndex(Index.Value, VisitMode); + } + + if (instance == null) + { + // No name, no index -> Exception + throw new InvalidDataException("Could not find actual Selection type, no selection criterion was provided."); + } + + if (SubSelections != null) + { + // Recursively convert children + var subSelections = SubSelections.Select(subSelection => subSelection.Convert()); + instance.SubSelections.AddRange(subSelections); + } + + return instance; + } + } + + /// + /// Visit mode for a Node + /// + public enum NodeVisitMode + { + /// + /// Visit the node and process it. + /// Continue visit according to the SubSelections. + /// + Default, + + /// + /// Visit the node but do not process it. + /// Let the SubSelections decide whether there are children nodes to be processed or not. + /// + PassThrough, + + /// + /// Visit the node and process it, ignore the SubSelections. + /// The visitor itself is solely responsible for the rest of the visit, independently of the SubSelections. + /// + Automatic + } + + /// + /// Abstract base class for a Node selection. + /// + internal abstract class Selection + { + /// + /// Defines what to do on the selected Node when reached. + /// + public NodeVisitMode VisitMode { get; } + + /// + /// Defines which nodes to select among the children of the current selected Node. + /// + public List SubSelections { get; } + + protected Selection(NodeVisitMode visitMode) + { + VisitMode = visitMode; + SubSelections = new List(); + } + + /// + /// Implements the selection mechanism defined for this instance + /// among the children of the given node. + /// + /// Parent node. + /// Node matching the selection criterion if any, null otherwise. + public abstract Node SelectChild(Node parent); + } + + internal class SelectionByName : Selection + { + /// + /// Name of Node to select. + /// + public string Name { get; } + + public SelectionByName(string name, NodeVisitMode visitMode) + : base(visitMode) + { + Name = name; + } + + public override Node SelectChild(Node parent) + { + // Optimization: use SymbolTable and filter on parent instead of iterating Children and filter by name + if (parent.SymbolTable.DataEntries.TryGetValue(Name, out var candidates)) + { + return candidates.FirstOrDefault(candidate => candidate.Parent == parent); + } + + return null; + } + } + + internal class SelectionByIndex : Selection + { + /// + /// Index of Node to select. + /// + public int Index { get; } + + public SelectionByIndex(int index, NodeVisitMode visitMode) + : base(visitMode) + { + Index = index; + } + + public override Node SelectChild(Node parent) + { + return Index < parent.ChildrenCount ? parent.Children[Index] : null; + } + } + + /// + /// Singleton instance of Selection to represent the automatic mode. + /// + internal class AutoSelection : Selection + { + public static AutoSelection Instance = new(); + + private AutoSelection() + : base(NodeVisitMode.Automatic) + { + + } + + public override Node SelectChild(Node parent) + { + // Should not be called as the selection has to be done by visitor itself in Automatic mode. + throw new InvalidOperationException("AutoSelection has no selection mechanism proper."); + } + } + + /// + /// Base class for AST visitor with a selection of nodes to visit. + /// + internal abstract class SelectedNodeVisitor + { + private readonly Selection _rootSelection; + + protected SelectedNodeVisitor(Selection rootSelection) + { + _rootSelection = rootSelection; + } + + /// + /// Starts the visit on the supplied root node, using the selection + /// provided at construction time. + /// + /// Root node to start the visit on. + public void Visit(Node root) + { + // Validate initial selection + bool startVisit = root.Parent == null || _rootSelection.SelectChild(root.Parent) == root; + if (startVisit) + { + VisitNode(_rootSelection, root); + } + } + + private void VisitChildren(Selection selection, Node parentNode) + { + if (selection.VisitMode == NodeVisitMode.Automatic) + { + // We are in automatic mode: use the selection mechanism from this visitor + foreach (var child in SelectChildren(parentNode)) + { + // Keep the automatic mode active + VisitNode(AutoSelection.Instance, child); + } + } + else + { + // Use selection mechanism from sub-selections + foreach (var subSelection in selection.SubSelections) + { + var child = subSelection.SelectChild(parentNode); + if (child != null) + { + VisitNode(subSelection, child); + } + // else: inconsistent selection from client ? Ignore. + } + } + } + + /// + /// Custom selection mechanism for this visitor, used when in Automatic mode. + /// + /// Parent node from which a set of children must be selected. + /// Non-null enumeration of selected children. + protected abstract IEnumerable SelectChildren(Node parent); + + private void VisitNode(Selection selection, Node node) + { + EnterNode(node); + if (selection.VisitMode != NodeVisitMode.PassThrough) + { + // Default or Automatic mode -> the node must be processed. + bool nodeHasSelectedChildren = selection.SubSelections.Count > 0 || selection.VisitMode == NodeVisitMode.Automatic; + ProcessNode(node, nodeHasSelectedChildren); + } + VisitChildren(selection, node); + ExitNode(node); + } + + /// + /// Called when entering a new node during the visit. + /// This method is called no matter the current visit mode. + /// + /// Newly entered node. + protected abstract void EnterNode(Node node); + + /// + /// Called when a Node has been selected for processing. + /// + /// The node to process. + /// Boolean indicating whether the node has selected children or not. + protected abstract void ProcessNode(Node node, bool nodeHasSelectedChildren); + + /// + /// Called when exiting a Node. + /// This method is called on every node previously entered, no matter the current visit mode. + /// + /// Exited node. + protected abstract void ExitNode(Node node); + } +} diff --git a/TypeCobol.LanguageServer/Utilities/CodeElementLocator.cs b/TypeCobol.LanguageServer/Utilities/CodeElementLocator.cs index 7d799d761..d88bdb14a 100644 --- a/TypeCobol.LanguageServer/Utilities/CodeElementLocator.cs +++ b/TypeCobol.LanguageServer/Utilities/CodeElementLocator.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using TypeCobol.Compiler; using TypeCobol.Compiler.CodeElements; using TypeCobol.Compiler.Nodes; diff --git a/TypeCobol/Compiler/Diagnostics/CrossChecker.cs b/TypeCobol/Compiler/Diagnostics/CrossChecker.cs index d26f1982a..cd61d69a7 100644 --- a/TypeCobol/Compiler/Diagnostics/CrossChecker.cs +++ b/TypeCobol/Compiler/Diagnostics/CrossChecker.cs @@ -807,7 +807,7 @@ public override bool Visit(DataDefinition dataDefinition) } } - if (HasChildrenThatDeclareData(dataDefinition))//Check if group is valid + if (dataDefinition.HasChildrenThatDeclareData())//Check if group is valid { if (dataDefinition.Picture != null) { @@ -947,42 +947,6 @@ public override bool Visit(FunctionEnd functionEnd) return true; } - /// - /// Test if the received DataDefinition has other children than DataConditionEntry or DataRenamesEntry - /// - /// Item to check - /// True if there are only DataConditionEntry or DataRenamesEntry children - private static bool HasChildrenThatDeclareData([NotNull] DataDefinition dataDefinition) - { - //We only need to check the last children: - //DataConditionEntry is a level 88, DataRenamesEntry is level 66 and they cannot have children - //DataDescription and DataRedefines are level between 1 and 49 inclusive. - //As the level number drives the positioning of Node inside the Children: - //- DataConditionEntry will always be positioned before other dataDescription - //- DataRenamesEntry will always be positioned after other dataDescription - if (dataDefinition.ChildrenCount > 0) - { - var lastChild = dataDefinition.Children[dataDefinition.ChildrenCount - 1]; - - if (lastChild.CodeElement == null) - { - Debug.Assert(lastChild is IndexDefinition); - //Last child is an Index in an OCCURS: it is not a declaration - return false; - } - - if (lastChild.CodeElement.Type == CodeElementType.DataRenamesEntry) - { - //Last child is a DataRenamesEntry: we need to loop on the other children to find a possible DataDescription before - return dataDefinition.Children.Any(c => c is DataDescription); - } - - return lastChild.CodeElement.Type != CodeElementType.DataConditionEntry; - } - - return false; - } - public override bool Visit(IndexDefinition indexDefinition) { var found = diff --git a/TypeCobol/Compiler/Nodes/Data.cs b/TypeCobol/Compiler/Nodes/Data.cs index 81e87d3e3..c07fd83db 100644 --- a/TypeCobol/Compiler/Nodes/Data.cs +++ b/TypeCobol/Compiler/Nodes/Data.cs @@ -1,18 +1,14 @@ +using System.Diagnostics; using System.Text; using JetBrains.Annotations; +using TypeCobol.Compiler.CodeElements; using TypeCobol.Compiler.Parser; +using TypeCobol.Compiler.Scanner; using TypeCobol.Compiler.Text; using TypeCobol.Compiler.Types; namespace TypeCobol.Compiler.Nodes { - using System; - using System.Collections.Generic; - using Scanner; - using TypeCobol.Compiler.CodeElements; - - - public class DataDivision: GenericNode, Parent { public const string NODE_ID = "data-division"; @@ -29,7 +25,7 @@ private enum SectionIndex LinkageSection } // The 5 (optional) data sections - private DataSection[] _sections = new DataSection[5]; + private readonly DataSection[] _sections = new DataSection[5]; public FileSection FileSection => (FileSection) _sections[(int)SectionIndex.FileSection]; public GlobalStorageSection GlobalStorageSection => (GlobalStorageSection)_sections[(int)SectionIndex.GlobalStorageSection]; public WorkingStorageSection WorkingStorageSection => (WorkingStorageSection)_sections[(int)SectionIndex.WorkingStorageSection]; @@ -1127,6 +1123,69 @@ public static bool IsFiller([CanBeNull] this DataDefinition dataDefinition) && dataDefinition.ChildrenCount == 0 // No children && dataDefinition.CodeElement.IsFiller(); // Anonymous or FILLER (by using keyword or FILLER-like name) } + + /// + /// Test if the received DataDefinition has other children than DataConditionEntry or DataRenamesEntry + /// + /// Non-null item to check + /// True if there are only DataConditionEntry or DataRenamesEntry children + public static bool HasChildrenThatDeclareData([NotNull] this DataDefinition dataDefinition) + { + //We only need to check the last children: + //DataConditionEntry is a level 88, DataRenamesEntry is level 66 and they cannot have children + //DataDescription and DataRedefines are level between 1 and 49 inclusive. + //As the level number drives the positioning of Node inside the Children: + //- DataConditionEntry will always be positioned before other dataDescription + //- DataRenamesEntry will always be positioned after other dataDescription + if (dataDefinition.ChildrenCount > 0) + { + var lastChild = dataDefinition.Children[dataDefinition.ChildrenCount - 1]; + + if (lastChild.CodeElement == null) + { + Debug.Assert(lastChild is IndexDefinition); + //Last child is an Index in an OCCURS: it is not a declaration + return false; + } + + if (lastChild.CodeElement.Type == CodeElementType.DataRenamesEntry) + { + //Last child is a DataRenamesEntry: we need to loop on the other children to find a possible DataDescription before + return dataDefinition.Children.Any(c => c is DataDescription); + } + + return lastChild.CodeElement.Type != CodeElementType.DataConditionEntry; + } + + return false; + } + + /// + /// Test the given data to check whether its PICTURE is among the given Picture categories. + /// + /// Non-null data to test. + /// Enumeration of Picture Categories to test, passing no categories will simply test whether + /// the data has a PICTURE type or not. + /// True when the actual picture category of the data is among the given categories. + public static bool HasPictureCategory([NotNull] this DataDefinition dataDefinition, params PictureCategory[] pictureCategories) + { + var type = dataDefinition.SemanticData?.Type; + bool hasPicture = type?.Tag == Types.Type.Tags.Picture; + if (hasPicture) + { + if (pictureCategories.Length == 0) + return true; // No categories to test, the method will just indicate that the data has a PICTURE + + var pictureType = (PictureType)type; + return pictureCategories.Contains(pictureType.Category); // We could use HashSet, but we're assuming a small array of PictureCategory without duplicates + } + + // No PICTURE + return false; + } + + public static bool IsNationalOrNationalEdited([NotNull] this DataDefinition dataDefinition) + => dataDefinition.HasPictureCategory(PictureCategory.National, PictureCategory.NationalEdited); } } // end of namespace TypeCobol.Compiler.Nodes