Skip to content

Commit 624360c

Browse files
author
Julia Pham
committed
feat: added structured error reports
1 parent 295f842 commit 624360c

8 files changed

Lines changed: 559 additions & 62 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package de.vill.exception;
2+
3+
public enum ErrorCategory {
4+
LEXICAL,
5+
SYNTAX,
6+
CONTEXT
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package de.vill.exception;
2+
3+
public enum ErrorField {
4+
FEATURE,
5+
CONSTRAINT,
6+
ATTRIBUTE,
7+
IMPORT,
8+
EXPRESSION,
9+
LANGUAGE_LEVEL,
10+
GROUP
11+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package de.vill.exception;
2+
3+
public class ErrorReport {
4+
5+
private final int line;
6+
private final int charPosition;
7+
private final ErrorCategory category;
8+
private final ErrorField field;
9+
private final String message;
10+
private final String reference;
11+
private final String cause;
12+
private final String hint;
13+
14+
private ErrorReport(Builder builder) {
15+
this.line = builder.line;
16+
this.charPosition = builder.charPosition;
17+
this.category = builder.category;
18+
this.field = builder.field;
19+
this.message = builder.message;
20+
this.reference = builder.reference;
21+
this.cause = builder.cause;
22+
this.hint = builder.hint;
23+
}
24+
25+
public int getLine() {
26+
return line;
27+
}
28+
29+
public int getCharPosition() {
30+
return charPosition;
31+
}
32+
33+
public ErrorCategory getCategory() {
34+
return category;
35+
}
36+
37+
public ErrorField getField() {
38+
return field;
39+
}
40+
41+
public String getMessage() {
42+
return message;
43+
}
44+
45+
public String getReference() {
46+
return reference;
47+
}
48+
49+
public String getCause() {
50+
return cause;
51+
}
52+
53+
public String getHint() {
54+
return hint;
55+
}
56+
57+
@Override
58+
public String toString() {
59+
StringBuilder sb = new StringBuilder();
60+
sb.append("[").append(category).append("] ");
61+
sb.append(message);
62+
sb.append(" (at ").append(line).append(":").append(charPosition).append(")");
63+
if (reference != null) {
64+
sb.append("\n Element: ").append(reference);
65+
}
66+
if (field != null) {
67+
sb.append("\n Field: ").append(field);
68+
}
69+
if (cause != null) {
70+
sb.append("\n Cause: ").append(cause);
71+
}
72+
if (hint != null) {
73+
sb.append("\n Hint: ").append(hint);
74+
}
75+
return sb.toString();
76+
}
77+
78+
public static class Builder {
79+
private int line;
80+
private int charPosition;
81+
private ErrorCategory category;
82+
private ErrorField field;
83+
private String message;
84+
private String reference;
85+
private String cause;
86+
private String hint;
87+
88+
public Builder(ErrorCategory category, String message) {
89+
this.category = category;
90+
this.message = message;
91+
}
92+
93+
public Builder line(int line) {
94+
this.line = line;
95+
return this;
96+
}
97+
98+
public Builder charPosition(int charPosition) {
99+
this.charPosition = charPosition;
100+
return this;
101+
}
102+
103+
public Builder field(ErrorField field) {
104+
this.field = field;
105+
return this;
106+
}
107+
108+
public Builder reference(String reference) {
109+
this.reference = reference;
110+
return this;
111+
}
112+
113+
public Builder cause(String cause) {
114+
this.cause = cause;
115+
return this;
116+
}
117+
118+
public Builder hint(String hint) {
119+
this.hint = hint;
120+
return this;
121+
}
122+
123+
public ErrorReport build() {
124+
return new ErrorReport(this);
125+
}
126+
}
127+
}

src/main/java/de/vill/exception/ParseError.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public class ParseError extends RuntimeException {
66

77
private int line = 0;
88
private int charPositionInLine = 0;
9+
private ErrorReport report;
910

1011
public ParseError(int line, int charPositionInLine, String errorMessage, Throwable err) {
1112
super(errorMessage, err);
@@ -21,13 +22,27 @@ public ParseError(String errorMessage, int line) {
2122
super(errorMessage);
2223
this.line = line;
2324
}
24-
25+
26+
public ParseError(ErrorReport report) {
27+
super(report.getMessage());
28+
this.line = report.getLine();
29+
this.charPositionInLine = report.getCharPosition();
30+
this.report = report;
31+
}
32+
2533
public int getLine() {
2634
return line;
2735
}
2836

37+
public ErrorReport getReport() {
38+
return report;
39+
}
40+
2941
@Override
3042
public String toString() {
43+
if (report != null) {
44+
return report.toString();
45+
}
3146
return String.format("%s (at %d:%d)", getMessage(), line, charPositionInLine);
3247
}
3348
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package de.vill.main;
2+
3+
import de.vill.exception.ErrorCategory;
4+
import de.vill.exception.ErrorField;
5+
import de.vill.exception.ErrorReport;
6+
import de.vill.exception.ParseError;
7+
import org.antlr.v4.runtime.*;
8+
9+
import java.util.List;
10+
import java.util.regex.*;
11+
12+
public class UVLErrorListener extends BaseErrorListener {
13+
14+
private final List<ParseError> errorList;
15+
16+
public UVLErrorListener(List<ParseError> errorList) {
17+
this.errorList = errorList;
18+
}
19+
20+
@Override
21+
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPosition, String message, RecognitionException e) {
22+
ErrorReport report = translateToReport(message, offendingSymbol, recognizer, line, charPosition);
23+
errorList.add(new ParseError(report));
24+
}
25+
26+
private ErrorReport translateToReport(String message, Object offendingSymbol, Recognizer<?, ?> recognizer, int line, int charPosition) {
27+
// Token recognition error -> LEXICAL
28+
Matcher m = Pattern.compile("token recognition error at: '(.*)'").matcher(message);
29+
if (m.find()) {
30+
String character = m.group(1);
31+
return new ErrorReport.Builder(ErrorCategory.LEXICAL,
32+
"Unexpected character '" + character + "'")
33+
.line(line).charPosition(charPosition)
34+
.reference(character)
35+
.cause("The character '" + character + "' is not supported in UVL.")
36+
.hint("Remove or replace the unsupported character.")
37+
.build();
38+
}
39+
40+
// Missing token -> SYNTAX
41+
m = Pattern.compile("missing '?([<>\\w]+)'? at '?(.*?)'?$").matcher(message);
42+
if (m.find()) {
43+
String missing = tokenToReadable(m.group(1));
44+
String found = tokenToReadable(m.group(2));
45+
return new ErrorReport.Builder(ErrorCategory.SYNTAX,
46+
"Missing " + missing + " before " + found)
47+
.line(line).charPosition(charPosition)
48+
.cause("Expected " + missing + " but found " + found + " instead.")
49+
.hint("Add " + missing + " before " + found + ".")
50+
.build();
51+
}
52+
53+
// Extraneous input -> SYNTAX
54+
m = Pattern.compile("extraneous input '?(.*?)'? expecting (.*)").matcher(message);
55+
if (m.find()) {
56+
String extra = tokenToReadable(m.group(1));
57+
return new ErrorReport.Builder(ErrorCategory.SYNTAX,
58+
"Unexpected input " + extra)
59+
.line(line).charPosition(charPosition)
60+
.reference(m.group(1))
61+
.cause("Found " + extra + " where a valid UVL element was expected.")
62+
.hint("Remove the unexpected input or check the indentation.")
63+
.build();
64+
}
65+
66+
// Mismatched input -> SYNTAX
67+
m = Pattern.compile("mismatched input '?(.*?)'? expecting (.*)").matcher(message);
68+
if (m.find()) {
69+
String found = tokenToReadable(m.group(1));
70+
String expected = simplifySet(m.group(2));
71+
72+
// Sonderfall: nach Gruppierung ohne Features
73+
// ANTLR meldet entweder "a new line" oder ein Gruppen-Keyword als found,
74+
// wenn eine Gruppe leer ist und expected "an indentation" ist
75+
if (expected.equals("an indentation") && (found.equals("a new line")
76+
|| found.contains("'mandatory'") || found.contains("'optional'")
77+
|| found.contains("'alternative'") || found.contains("'or'"))) {
78+
return new ErrorReport.Builder(ErrorCategory.SYNTAX,
79+
"Missing features after group type")
80+
.line(line).charPosition(charPosition)
81+
.field(ErrorField.GROUP)
82+
.cause("A group type ('optional', 'or', 'mandatory', 'alternative') must be followed by at least one feature.")
83+
.hint("Add at least one child feature below the group type.")
84+
.build();
85+
}
86+
87+
// Sonderfall: mehr als ein root Feature
88+
if (expected.equals("an indentation or a dedentation")) {
89+
return new ErrorReport.Builder(ErrorCategory.SYNTAX,
90+
"More than one root feature detected")
91+
.line(line).charPosition(charPosition)
92+
.cause("A UVL model can only have one root feature.")
93+
.hint("Check if a group type ('mandatory', 'optional', 'or', 'alternative') is missing.")
94+
.build();
95+
}
96+
97+
return new ErrorReport.Builder(ErrorCategory.SYNTAX,
98+
"Found " + found + " but expected " + expected)
99+
.line(line).charPosition(charPosition)
100+
.cause("The parser expected " + expected + " at this position.")
101+
.hint("Replace " + found + " with " + expected + ".")
102+
.build();
103+
}
104+
105+
// No viable alternative
106+
m = Pattern.compile("no viable alternative at input '(.*)'").matcher(message);
107+
if (m.find()) {
108+
String input = m.group(1).replace("\n", "").trim();
109+
return new ErrorReport.Builder(ErrorCategory.SYNTAX,
110+
"No valid interpretation for '" + input + "'")
111+
.line(line).charPosition(charPosition)
112+
.reference(input)
113+
.cause("The input '" + input + "' does not match any valid UVL structure at this position.")
114+
.hint("Check if a keyword like 'features', 'constraints', or 'imports' is missing, or if the indentation is correct.")
115+
.build();
116+
}
117+
118+
// Fallback
119+
return new ErrorReport.Builder(ErrorCategory.SYNTAX, message)
120+
.line(line).charPosition(charPosition)
121+
.cause("An unexpected syntax error occurred.")
122+
.hint("Check the UVL syntax around this position.")
123+
.build();
124+
}
125+
126+
// Translating ANTLR tokens
127+
private String tokenToReadable(String token) {
128+
switch (token) {
129+
case "<INDENT>": return "an indentation";
130+
case "<DEDENT>": return "a dedentation";
131+
case "<EOF>": case "EOF": return "end of file";
132+
case "\\n": return "a new line";
133+
case "features": return "'features' keyword";
134+
case "constraints": return "'constraints' keyword";
135+
case "imports": return "'imports' keyword";
136+
case "mandatory": return "'mandatory' group";
137+
case "optional": return "'optional' group";
138+
case "alternative": return "'alternative' group";
139+
case "or": return "'or' group";
140+
default: return "'" + token + "'";
141+
}
142+
}
143+
144+
private String simplifySet(String expectedSet) {
145+
String cleaned = expectedSet.replaceAll("[{}]", "").trim();
146+
String[] tokens = cleaned.split(",\\s*");
147+
StringBuilder sb = new StringBuilder();
148+
for (int i = 0; i < tokens.length; i++) {
149+
if (i > 0) {
150+
if (i == tokens.length - 1) {
151+
sb.append(" or ");
152+
} else {
153+
sb.append(", ");
154+
}
155+
}
156+
sb.append(tokenToReadable(tokens[i].replace("'", "").trim()));
157+
}
158+
return sb.toString();
159+
}
160+
}

0 commit comments

Comments
 (0)