Skip to content

Commit 45ef3a2

Browse files
committed
#792 Change the syntax error exception definition to include position in error line.
1 parent 47ebf5f commit 45ef3a2

File tree

8 files changed

+68
-30
lines changed

8 files changed

+68
-30
lines changed

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/antlr/ANTLRParser.scala

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ import za.co.absa.cobrix.cobol.parser.policies.StringTrimmingPolicy.StringTrimmi
2929
import java.nio.charset.Charset
3030

3131

32-
class ThrowErrorStrategy() extends DefaultErrorStrategy {
32+
class ThrowErrorStrategy(posAdjustment: Int) extends DefaultErrorStrategy {
3333
override def recover(recognizer: Parser, e: RecognitionException): Unit = {
3434
throw new SyntaxErrorException(
3535
e.getOffendingToken.getLine,
36-
"",
36+
Option(e.getOffendingToken.getCharPositionInLine + posAdjustment),
37+
None,
3738
"Invalid input " + getTokenErrorDisplay(e.getOffendingToken) + " at position " + e.getOffendingToken.getLine
38-
+ ":" + (e.getOffendingToken.getCharPositionInLine + 6)
39+
+ ":" + (e.getOffendingToken.getCharPositionInLine + posAdjustment)
3940
)
4041
}
4142

@@ -65,8 +66,9 @@ object ANTLRParser extends Logging {
6566
isUtf16BigEndian: Boolean,
6667
floatingPointFormat: FloatingPointFormat,
6768
fieldCodePageMap: Map[String, String]): CopybookAST = {
68-
val visitor = new ParserVisitor(enc, stringTrimmingPolicy, isDisplayAlwaysString, ebcdicCodePage, asciiCharset, isUtf16BigEndian, floatingPointFormat, strictSignOverpunch, improvedNullDetection, strictIntegralPrecision, decodeBinaryAsHex, fieldCodePageMap)
69+
val visitor = new ParserVisitor(enc, stringTrimmingPolicy, commentPolicy, isDisplayAlwaysString, ebcdicCodePage, asciiCharset, isUtf16BigEndian, floatingPointFormat, strictSignOverpunch, improvedNullDetection, strictIntegralPrecision, decodeBinaryAsHex, fieldCodePageMap)
6970

71+
val adjPos = if (commentPolicy.truncateComments) commentPolicy.commentsUpToChar else 0
7072
val strippedContents = filterSpecialCharacters(copyBookContents).split("\\r?\\n").map(
7173
line =>
7274
truncateComments(line, commentPolicy)
@@ -81,7 +83,7 @@ object ANTLRParser extends Logging {
8183
val parser = new copybookParser(tokens)
8284
parser.removeErrorListeners()
8385
parser.addErrorListener(new LogErrorListener(logger))
84-
parser.setErrorHandler(new ThrowErrorStrategy())
86+
parser.setErrorHandler(new ThrowErrorStrategy(adjPos))
8587

8688
visitor.visitMain(parser.main())
8789
visitor.ast

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/antlr/ParserVisitor.scala

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import za.co.absa.cobrix.cobol.parser.decoders.FloatingPointFormat.FloatingPoint
2828
import za.co.absa.cobrix.cobol.parser.encoding.codepage.CodePage
2929
import za.co.absa.cobrix.cobol.parser.encoding._
3030
import za.co.absa.cobrix.cobol.parser.exceptions.SyntaxErrorException
31+
import za.co.absa.cobrix.cobol.parser.policies.CommentPolicy
3132
import za.co.absa.cobrix.cobol.parser.policies.StringTrimmingPolicy.StringTrimmingPolicy
3233
import za.co.absa.cobrix.cobol.parser.position.{Left, Position, Right}
3334

@@ -41,6 +42,7 @@ sealed trait Expr
4142

4243
class ParserVisitor(enc: Encoding,
4344
stringTrimmingPolicy: StringTrimmingPolicy,
45+
commentPolicy: CommentPolicy,
4446
isDisplayAlwaysString: Boolean,
4547
ebcdicCodePage: CodePage,
4648
asciiCharset: Charset,
@@ -158,19 +160,19 @@ class ParserVisitor(enc: Encoding,
158160
pic.value match {
159161
case dec: Decimal =>
160162
if (dec.compact.isDefined && !dec.compact.contains(usageVal))
161-
throw new SyntaxErrorException(ctx.start.getLine, "", s"Field USAGE (${dec.compact.get}) doesn't match group's USAGE ($usageVal).")
163+
throw new SyntaxErrorException(ctx.start.getLine, Option(ctx.start.getCharPositionInLine), None, s"Field USAGE (${dec.compact.get}) doesn't match group's USAGE ($usageVal).")
162164
dec.copy(compact=usage)
163165
case int: Integral =>
164166
if (int.compact.isDefined && !int.compact.contains(usageVal))
165-
throw new SyntaxErrorException(ctx.start.getLine, "", s"Field USAGE (${int.compact.get}) doesn't match group's USAGE ($usageVal).")
167+
throw new SyntaxErrorException(ctx.start.getLine, Option(ctx.start.getCharPositionInLine), None, s"Field USAGE (${int.compact.get}) doesn't match group's USAGE ($usageVal).")
166168
int.copy(compact=usage)
167169
case x: AlphaNumeric if usageVal == COMP3U() =>
168170
Integral(x.pic, x.length*2, None, false, None, Some(COMP3U()), None, x.originalPic)
169171
case x: AlphaNumeric if usageVal == COMP1() || usageVal == COMP4() =>
170172
val enc = if (decodeBinaryAsHex) HEX else RAW
171173
x.copy(compact=usage, enc=Some(enc))
172174
case x: AlphaNumeric =>
173-
throw new SyntaxErrorException(ctx.start.getLine, "", s"Field USAGE $usageVal is not supported with this PIC: ${x.pic}. The field should be numeric.")
175+
throw new SyntaxErrorException(ctx.start.getLine, Option(ctx.start.getCharPositionInLine), None, s"Field USAGE $usageVal is not supported with this PIC: ${x.pic}. The field should be numeric.")
174176
}
175177
)
176178
}
@@ -226,7 +228,7 @@ class ParserVisitor(enc: Encoding,
226228
case None => addLevel(section)
227229
case Some(s) if s > section => addLevel(section)
228230
case _ =>
229-
throw new SyntaxErrorException(levels.top.el.children.last.lineNumber, levels.top.el.children.last.name,
231+
throw new SyntaxErrorException(levels.top.el.children.last.lineNumber, None, Option(levels.top.el.children.last.name),
230232
s"The field is a leaf element and cannot contain nested fields.")
231233
}
232234

@@ -556,35 +558,37 @@ class ParserVisitor(enc: Encoding,
556558
}
557559

558560
def checkBounds(ctx: ParserRuleContext, expr: PicExpr): PicExpr = {
561+
val adjustPos = if (commentPolicy.truncateComments) commentPolicy.commentsUpToChar + 1 else 1
562+
val pos = Option(ctx.stop.getCharPositionInLine + adjustPos)
559563
expr.value match {
560564
case x: Decimal =>
561565
if (x.isSignSeparate && x.compact.isDefined)
562-
throw new SyntaxErrorException(ctx.start.getLine, getIdentifier(ctx.parent),
566+
throw new SyntaxErrorException(ctx.start.getLine, None, Option(getIdentifier(ctx.parent)),
563567
s"SIGN SEPARATE clause is not supported for ${x.compact.get}. It is only supported for DISPLAY formatted fields.")
564568
if(x.scale > Constants.maxDecimalScale)
565-
throw new SyntaxErrorException(ctx.start.getLine, getIdentifier(ctx.parent),
569+
throw new SyntaxErrorException(ctx.start.getLine, pos, Option(getIdentifier(ctx.parent)),
566570
s"Decimal numbers with scale bigger than ${Constants.maxDecimalScale} are not supported.")
567571
if(x.precision > Constants.maxDecimalPrecision)
568-
throw new SyntaxErrorException(ctx.start.getLine, getIdentifier(ctx.parent),
572+
throw new SyntaxErrorException(ctx.start.getLine, pos, Option(getIdentifier(ctx.parent)),
569573
s"Decimal numbers with precision bigger than ${Constants.maxDecimalPrecision} are not supported.")
570574
if (x.compact.isDefined && x.explicitDecimal)
571-
throw new SyntaxErrorException(ctx.start.getLine, getIdentifier(ctx.parent),
575+
throw new SyntaxErrorException(ctx.start.getLine, pos, Option(getIdentifier(ctx.parent)),
572576
s"Explicit decimal point in 'PIC ${expr.value.originalPic.get}' is not supported for ${x.compact.get}. It is only supported for DISPLAY formatted fields.")
573577
case x: Integral =>
574578
if (x.isSignSeparate && x.compact.isDefined) {
575-
throw new SyntaxErrorException(ctx.start.getLine, getIdentifier(ctx.parent),
579+
throw new SyntaxErrorException(ctx.start.getLine, None, Option(getIdentifier(ctx.parent)),
576580
s"SIGN SEPARATE clause is not supported for ${x.compact.get}. It is only supported for DISPLAY formatted fields.")
577581
}
578582
if (x.precision > Constants.maxBinIntPrecision && x.compact.contains(COMP4())) {
579-
throw new SyntaxErrorException(ctx.start.getLine, getIdentifier(ctx.parent),
583+
throw new SyntaxErrorException(ctx.start.getLine, pos, Option(getIdentifier(ctx.parent)),
580584
s"BINARY-encoded integers with precision bigger than ${Constants.maxBinIntPrecision} are not supported.")
581585
}
582586
if (x.precision < 1 || x.precision >= Constants.maxFieldLength)
583-
throw new SyntaxErrorException(ctx.start.getLine, getIdentifier(ctx.parent),
587+
throw new SyntaxErrorException(ctx.start.getLine, pos, Option(getIdentifier(ctx.parent)),
584588
s"Incorrect field size of ${x.precision} for PIC ${expr.value.originalPic.get}. Supported size is in range from 1 to ${Constants.maxFieldLength}.")
585589
case x: AlphaNumeric =>
586590
if (x.length < 1 || x.length >= Constants.maxFieldLength)
587-
throw new SyntaxErrorException(ctx.start.getLine, getIdentifier(ctx.parent),
591+
throw new SyntaxErrorException(ctx.start.getLine, pos, Option(getIdentifier(ctx.parent)),
588592
s"Incorrect field size of ${x.length} for PIC ${expr.value.originalPic.get}. Supported size is in range from 1 to ${Constants.maxFieldLength}.")
589593
}
590594
expr

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/asttransform/BinaryPropertiesAdder.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ class BinaryPropertiesAdder extends AstTransformer {
5353
redefinedNames.clear()
5454
case Some(redefines) =>
5555
if (i == 0) {
56-
throw new SyntaxErrorException(child.lineNumber, child.name, s"The first field of a group cannot use REDEFINES keyword.")
56+
throw new SyntaxErrorException(child.lineNumber, None, Option(child.name), s"The first field of a group cannot use REDEFINES keyword.")
5757
}
5858
if (!redefinedNames.contains(redefines.toUpperCase)) {
59-
throw new SyntaxErrorException(child.lineNumber, child.name, s"The field ${child.name} redefines $redefines, which is not part if the redefined fields block.")
59+
throw new SyntaxErrorException(child.lineNumber, None, Option(child.name), s"The field ${child.name} redefines $redefines, which is not part if the redefined fields block.")
6060
}
6161
newChildren(i - 1) = newChildren(i - 1).withUpdatedIsRedefined(newIsRedefined = true)
6262
}

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/exceptions/SyntaxErrorException.scala

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,26 @@
1616

1717
package za.co.absa.cobrix.cobol.parser.exceptions
1818

19-
class SyntaxErrorException(val lineNumber: Int, val field: String, val msg: String)
20-
extends Exception(SyntaxErrorException.constructErrorMessage(lineNumber, field, msg)) {
19+
class SyntaxErrorException(val lineNumber: Int, val posOpt: Option[Int], val fieldOpt: Option[String], val msg: String)
20+
extends Exception(SyntaxErrorException.constructErrorMessage(lineNumber, posOpt, fieldOpt, msg)) {
2121
}
2222

2323
object SyntaxErrorException {
24-
private def constructErrorMessage(lineNumber: Int, field: String, msg: String): String = {
25-
val atLine = if (lineNumber > 0) s" at line $lineNumber"
26-
val atField = if (field.nonEmpty) s", field $field" else ""
24+
private def constructErrorMessage(lineNumber: Int, pos: Option[Int], fieldOpt: Option[String], msg: String): String = {
25+
val atLine = if (lineNumber > 0) {
26+
pos match {
27+
case Some(p) => s" at line $lineNumber:$p"
28+
case None => s" at line $lineNumber"
29+
}
30+
}
31+
else
32+
""
33+
34+
val atField = fieldOpt match {
35+
case Some(f) => s", field $f"
36+
case None => ""
37+
}
2738

2839
s"Syntax error in the copybook$atLine$atField: $msg"
2940
}
30-
}
41+
}

cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/StringDecodersSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ class StringDecodersSpec extends AnyWordSpec {
260260
0xAA, 0xAB, 0xAC, 0xAD, 0x8C, 0x8E, 0x80, 0xB6, 0xB3, 0xB5, 0xB7, 0xB1, 0xB0, 0xB4, 0x76, 0xA0,
261261
0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
262262
0xD8, 0xD9, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6,
263-
0xF7, 0xF8, 0xF9, 0x4A, 0x5A, 0x25, 0x0D,
263+
0xF7, 0xF8, 0xF9, 0x4A, 0x5A, 0x25, 0x0D
264264
).map(_.toByte)
265265

266266
val enc = new CodePage1025

cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/DataSizeSpec.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import za.co.absa.cobrix.cobol.parser.ast.{Group, Primitive}
2525
import za.co.absa.cobrix.cobol.parser.decoders.FloatingPointFormat
2626
import za.co.absa.cobrix.cobol.parser.encoding.ASCII
2727
import za.co.absa.cobrix.cobol.parser.encoding.codepage.CodePage
28-
import za.co.absa.cobrix.cobol.parser.policies.StringTrimmingPolicy
28+
import za.co.absa.cobrix.cobol.parser.policies.{CommentPolicy, StringTrimmingPolicy}
2929

3030
import java.nio.charset.StandardCharsets
3131

@@ -35,6 +35,7 @@ class DataSizeSpec extends AnyFunSuite {
3535
private def parse(pic: String): Primitive = {
3636
val visitor = new ParserVisitor(ASCII,
3737
StringTrimmingPolicy.TrimNone,
38+
CommentPolicy(),
3839
isDisplayAlwaysString = false,
3940
CodePage.getCodePageByName("common"),
4041
StandardCharsets.US_ASCII,
@@ -55,7 +56,7 @@ class DataSizeSpec extends AnyFunSuite {
5556
val parser = new copybookParser(tokens)
5657
parser.removeErrorListeners()
5758
parser.addErrorListener(new LogErrorListener(logger))
58-
parser.setErrorHandler(new ThrowErrorStrategy())
59+
parser.setErrorHandler(new ThrowErrorStrategy(6))
5960
visitor.visit(parser.main())
6061
visitor.ast.children.head.asInstanceOf[Group].children.head.asInstanceOf[Primitive]
6162
}

cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/PicValidationSpec.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import za.co.absa.cobrix.cobol.parser.decoders.FloatingPointFormat
2626
import za.co.absa.cobrix.cobol.parser.encoding.ASCII
2727
import za.co.absa.cobrix.cobol.parser.encoding.codepage.CodePage
2828
import za.co.absa.cobrix.cobol.parser.exceptions.SyntaxErrorException
29-
import za.co.absa.cobrix.cobol.parser.policies.StringTrimmingPolicy
29+
import za.co.absa.cobrix.cobol.parser.policies.{CommentPolicy, StringTrimmingPolicy}
3030

3131
class PicValidationSpec extends AnyFunSuite {
3232
private val logger: Logger = LoggerFactory.getLogger(this.getClass)
@@ -35,6 +35,7 @@ class PicValidationSpec extends AnyFunSuite {
3535

3636
val visitor = new ParserVisitor(ASCII,
3737
StringTrimmingPolicy.TrimNone,
38+
CommentPolicy(),
3839
isDisplayAlwaysString = false,
3940
CodePage.getCodePageByName("common"),
4041
StandardCharsets.UTF_8,
@@ -55,7 +56,7 @@ class PicValidationSpec extends AnyFunSuite {
5556
val parser = new copybookParser(tokens)
5657
parser.removeErrorListeners()
5758
parser.addErrorListener(new LogErrorListener(logger))
58-
parser.setErrorHandler(new ThrowErrorStrategy())
59+
parser.setErrorHandler(new ThrowErrorStrategy(6))
5960
visitor.visit(parser.main())
6061
}
6162

cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/parse/SyntaxErrorsSpec.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class SyntaxErrorsSpec extends AnyFunSuite {
4141
}
4242

4343
assert(syntaxErrorException.lineNumber == 5)
44+
assert(syntaxErrorException.posOpt.isEmpty)
45+
assert(syntaxErrorException.fieldOpt.contains("GRP_FIELD"))
4446
assert(syntaxErrorException.msg.contains("The field is a leaf element"))
4547
}
4648

@@ -57,6 +59,8 @@ class SyntaxErrorsSpec extends AnyFunSuite {
5759
}
5860

5961
assert(syntaxErrorException.lineNumber == 4)
62+
assert(syntaxErrorException.posOpt.isEmpty)
63+
assert(syntaxErrorException.fieldOpt.contains("SUB_FLD2"))
6064
assert(syntaxErrorException.msg.contains("The field SUB_FLD2 redefines SUB_FLD1, which is not part if the redefined fields block"))
6165
}
6266

@@ -70,6 +74,8 @@ class SyntaxErrorsSpec extends AnyFunSuite {
7074
CopybookParser.parseTree(copyBookContents)
7175
}
7276
assert(syntaxErrorException.lineNumber == 2)
77+
assert(syntaxErrorException.posOpt.contains(36))
78+
assert(syntaxErrorException.fieldOpt.contains("FIELD"))
7379
assert(syntaxErrorException.msg.contains("Decimal numbers with precision bigger"))
7480
}
7581

@@ -83,6 +89,8 @@ class SyntaxErrorsSpec extends AnyFunSuite {
8389
CopybookParser.parseTree(copyBookContents)
8490
}
8591
assert(syntaxErrorException.lineNumber == 2)
92+
assert(syntaxErrorException.posOpt.contains(36))
93+
assert(syntaxErrorException.fieldOpt.contains("FIELD"))
8694
assert(syntaxErrorException.msg.contains("Decimal numbers with scale bigger"))
8795
}
8896

@@ -95,7 +103,10 @@ class SyntaxErrorsSpec extends AnyFunSuite {
95103
val syntaxErrorException = intercept[SyntaxErrorException] {
96104
CopybookParser.parseTree(copyBookContents)
97105
}
106+
98107
assert(syntaxErrorException.lineNumber == 2)
108+
assert(syntaxErrorException.posOpt.contains(8))
109+
assert(syntaxErrorException.fieldOpt.isEmpty)
99110
assert(syntaxErrorException.msg.contains("Invalid input '/' at position 2:8"))
100111
}
101112

@@ -110,6 +121,8 @@ class SyntaxErrorsSpec extends AnyFunSuite {
110121
CopybookParser.parseTree(copyBookContents)
111122
}
112123
assert(syntaxErrorException.lineNumber == 2)
124+
assert(syntaxErrorException.posOpt.isEmpty)
125+
assert(syntaxErrorException.fieldOpt.contains("FIELD"))
113126
assert(syntaxErrorException.msg.contains("SIGN SEPARATE clause is not supported for COMP-3"))
114127
}
115128

@@ -123,6 +136,8 @@ class SyntaxErrorsSpec extends AnyFunSuite {
123136
CopybookParser.parseTree(copyBookContents)
124137
}
125138
assert(syntaxErrorException.lineNumber == 2)
139+
assert(syntaxErrorException.posOpt.contains(38))
140+
assert(syntaxErrorException.fieldOpt.contains("FIELD"))
126141
assert(syntaxErrorException.msg.contains("Explicit decimal point in 'PIC 9(8).9(9)' is not supported for COMP-3."))
127142
}
128143

@@ -148,6 +163,8 @@ class SyntaxErrorsSpec extends AnyFunSuite {
148163
CopybookParser.parseTree(copyBookContents)
149164
}
150165
assert(syntaxErrorException.lineNumber == 2)
166+
assert(syntaxErrorException.posOpt.contains(32))
167+
assert(syntaxErrorException.fieldOpt.isEmpty)
151168
assert(syntaxErrorException.msg.contains("Invalid input"))
152169
assert(syntaxErrorException.msg.contains("at position 2:32"))
153170
}
@@ -162,6 +179,8 @@ class SyntaxErrorsSpec extends AnyFunSuite {
162179
CopybookParser.parseTree(copyBookContents)
163180
}
164181
assert(syntaxErrorException.lineNumber == 2)
182+
assert(syntaxErrorException.posOpt.contains(29))
183+
assert(syntaxErrorException.fieldOpt.isEmpty)
165184
assert(syntaxErrorException.msg.contains("Invalid input '(' at position 2:29"))
166185
}
167186

0 commit comments

Comments
 (0)