From 7f8bafa213252f19d8cf87be2b966dd96122af97 Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Thu, 29 Oct 2026 08:48:27 +0100 Subject: [PATCH 1/2] #878 Replace assertions in the copybook parser with descriptive syntax errors for duplicate REDEFINES, USAGE, OCCURS, VALUE, SIGN SEPARATE, and PIC clauses, and add tests. --- .../cobol/parser/antlr/ParserVisitor.scala | 34 +++-- .../cobol/parser/parse/SyntaxErrorsSpec.scala | 143 +++++++++++++++++- 2 files changed, 160 insertions(+), 17 deletions(-) diff --git a/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/antlr/ParserVisitor.scala b/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/antlr/ParserVisitor.scala index 70e2caff..c0aee5a6 100644 --- a/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/antlr/ParserVisitor.scala +++ b/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/antlr/ParserVisitor.scala @@ -503,14 +503,14 @@ class ParserVisitor(enc: Encoding, val section = ctx.section.getText.toInt val parent: Group = getParentFromLevel(section) - assert(ctx.redefines.size() < 2) - assert(ctx.usageGroup.size() < 2) - assert(ctx.occurs.size() < 2) - assert(ctx.values.size() < 2) - val identifier = visitIdentifier(ctx.identifier()).value val originalIdentifier = visitIdentifier(ctx.identifier()).originalValue + require(ctx.redefines.size() < 2, ctx, Option(identifier), "Duplicate REDEFINES clause") + require(ctx.usageGroup.size() < 2, ctx, Option(identifier), "Duplicate USAGE clause") + require(ctx.occurs.size() < 2, ctx, Option(identifier), "Duplicate OCCURS clause") + require(ctx.values.size() < 2, ctx, Option(identifier), "Duplicate VALUE clause") + val redefines: Option[String] = ctx.redefines().asScala.toList match { case Nil => None case x :: _ => Some(visitIdentifier(x.identifier).value) @@ -793,15 +793,17 @@ class ParserVisitor(enc: Encoding, val section = ctx.section.getText.toInt val parent: Group = getParentFromLevel(section) - assert(ctx.redefines.size() < 2) - assert(ctx.usage.size() < 2) - assert(ctx.occurs.size() < 2) - assert(ctx.separateSign.size() < 2) - assert(ctx.pic.size() == 1) - val identifier = visitIdentifier(ctx.identifier()).value val originalIdentifier = visitIdentifier(ctx.identifier()).originalValue + require(ctx.redefines.size() < 2, ctx, Option(identifier), "Duplicate REDEFINES clause") + require(ctx.usage.size() < 2, ctx, Option(identifier), "Duplicate USAGE clause") + require(ctx.occurs.size() < 2, ctx, Option(identifier), "Duplicate OCCURS clause") + require(ctx.separateSign.size() < 2, ctx, Option(identifier), "Duplicate SEPARATE SIGN clause") + require(ctx.values.size() < 2, ctx, Option(identifier), "Duplicate VALUE clause") + require(ctx.pic.size() < 2, ctx, Option(identifier), "Duplicate PIC clauses") + require(ctx.pic.size() > 0, ctx, Option(identifier), "Missing PIC clauses") + var pic: PicExpr = visitPic(ctx.pic(0)) val redefines: Option[String] = ctx.redefines().asScala.toList match { @@ -874,9 +876,9 @@ class ParserVisitor(enc: Encoding, override def visitLevel66statement(ctx: copybookParser.Level66statementContext): Expr = { throw new RuntimeException("Renames not supported yet") } -} - - - - + def require(condition: => Boolean, ctx: ParserRuleContext, field: Option[String], message: String): Unit = { + if (!condition) { + throw new SyntaxErrorException(ctx.start.getLine, None, field, message) + } + }} diff --git a/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala b/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala index 5b602a18..c45389be 100644 --- a/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala +++ b/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala @@ -19,7 +19,6 @@ package za.co.absa.cobrix.cobol.parser.parse import org.scalatest.funsuite.AnyFunSuite import org.slf4j.{Logger, LoggerFactory} import za.co.absa.cobrix.cobol.base.SimpleComparisonBase -import za.co.absa.cobrix.cobol.internal.Logging import za.co.absa.cobrix.cobol.parser.CopybookParser import za.co.absa.cobrix.cobol.parser.ast.{Group, Primitive} import za.co.absa.cobrix.cobol.parser.exceptions.SyntaxErrorException @@ -390,4 +389,146 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { assert(syntaxErrorException.lineNumber == 3) assert(syntaxErrorException.getMessage.toUpperCase.contains("REDEFINES")) } + + test("The multiple REDEFINES clause in primitive") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 PIC X(1). + | 10 FILED3 REDEFINES FILED1 REDEFINES FILED2 PIC X(1). + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 4) + assert(syntaxErrorException.getMessage.contains("Duplicate REDEFINES clause")) + } + + test("The multiple REDEFINES clause in group") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 PIC X(1). + | 10 FILED3 REDEFINES FILED1 REDEFINES FILED2. + | 15 FIELD3 PIC X(1). + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 4) + assert(syntaxErrorException.getMessage.contains("Duplicate REDEFINES clause")) + } + + test("The multiple OCCURS clause in primitive") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 PIC X(1) OCCURS 2 TIMES OCCURS 3 TIMES. + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.getMessage.contains("Duplicate OCCURS clause")) + } + + test("The multiple OCCURS clause in group") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 OCCURS 2 TIMES OCCURS 3 TIMES. + | 15 FIELD3 PIC X(1). + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.getMessage.contains("Duplicate OCCURS clause")) + } + + test("The multiple USAGE clause in group") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 USAGE COMP USAGE COMP. + | 15 FIELD3 PIC 9(4). + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.getMessage.contains("Duplicate USAGE clause")) + } + + test("The multiple SIGN SEPARATE clause in primitive") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 PIC S9(4) SIGN IS LEADING SEPARATE + | SIGN IS TRAILING SEPARATE. + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.getMessage.contains("Duplicate SEPARATE SIGN clause")) + } + + test("The multiple PIC clause in primitive") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 PIC X(1) PIC X(2). + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.getMessage.contains("Duplicate PIC clause")) + } + + test("The multiple VALUE clause in primitive") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 PIC X(1) VALUE 'A' VALUE 'B'. + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.getMessage.contains("Duplicate VALUE clause")) + } + + test("The multiple VALUE clause in group") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 VALUE 'A' VALUE 'B'. + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.getMessage.contains("Duplicate VALUE clause")) + } + } From fc1e038d85b50886889a9698fe50a802850d762f Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Fri, 30 Oct 2026 09:16:53 +0100 Subject: [PATCH 2/2] #878 Add field name assertions to duplicate clause syntax error tests, and add a test for duplicate USAGE clauses in primitive fields. --- .../cobol/parser/parse/SyntaxErrorsSpec.scala | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala b/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala index c45389be..4f940fe8 100644 --- a/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala +++ b/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala @@ -403,6 +403,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 4) + assert(syntaxErrorException.fieldOpt.contains("FILED3")) assert(syntaxErrorException.getMessage.contains("Duplicate REDEFINES clause")) } @@ -412,7 +413,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { | 10 FILED1 PIC X(1). | 10 FILED2 PIC X(1). | 10 FILED3 REDEFINES FILED1 REDEFINES FILED2. - | 15 FIELD3 PIC X(1). + | 15 FIELD4 PIC X(1). |""".stripMargin val syntaxErrorException = intercept[SyntaxErrorException] { @@ -420,6 +421,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 4) + assert(syntaxErrorException.fieldOpt.contains("FILED3")) assert(syntaxErrorException.getMessage.contains("Duplicate REDEFINES clause")) } @@ -435,6 +437,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.fieldOpt.contains("FILED2")) assert(syntaxErrorException.getMessage.contains("Duplicate OCCURS clause")) } @@ -451,6 +454,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.fieldOpt.contains("FILED2")) assert(syntaxErrorException.getMessage.contains("Duplicate OCCURS clause")) } @@ -467,6 +471,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.fieldOpt.contains("FILED2")) assert(syntaxErrorException.getMessage.contains("Duplicate USAGE clause")) } @@ -483,9 +488,26 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.fieldOpt.contains("FILED2")) assert(syntaxErrorException.getMessage.contains("Duplicate SEPARATE SIGN clause")) } + test("The multiple USAGE clause in primitive") { + val copyBookContents = + """ 01 RECORD. + | 10 FILED1 PIC X(1). + | 10 FILED2 PIC X(1) USAGE COMP USAGE COMP. + |""".stripMargin + + val syntaxErrorException = intercept[SyntaxErrorException] { + CopybookParser.parseTree(copyBookContents) + } + + assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.fieldOpt.contains("FILED2")) + assert(syntaxErrorException.getMessage.contains("Duplicate USAGE clause")) + } + test("The multiple PIC clause in primitive") { val copyBookContents = """ 01 RECORD. @@ -498,6 +520,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.fieldOpt.contains("FILED2")) assert(syntaxErrorException.getMessage.contains("Duplicate PIC clause")) } @@ -513,6 +536,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.fieldOpt.contains("FILED2")) assert(syntaxErrorException.getMessage.contains("Duplicate VALUE clause")) } @@ -528,6 +552,7 @@ class SyntaxErrorsSpec extends AnyFunSuite with SimpleComparisonBase { } assert(syntaxErrorException.lineNumber == 3) + assert(syntaxErrorException.fieldOpt.contains("FILED2")) assert(syntaxErrorException.getMessage.contains("Duplicate VALUE clause")) }