8324859: Improve error recovery

Reviewed-by: mcimadamore
This commit is contained in:
Jan Lahoda 2024-08-29 06:25:27 +00:00
parent 1383fec417
commit 0b4a7d5342
2 changed files with 622 additions and 4 deletions

View File

@ -59,6 +59,7 @@ import static com.sun.tools.javac.parser.Tokens.TokenKind.EQ;
import static com.sun.tools.javac.parser.Tokens.TokenKind.GT;
import static com.sun.tools.javac.parser.Tokens.TokenKind.IMPORT;
import static com.sun.tools.javac.parser.Tokens.TokenKind.LT;
import com.sun.tools.javac.parser.VirtualParser.VirtualScanner;
import static com.sun.tools.javac.tree.JCTree.Tag.*;
import static com.sun.tools.javac.resources.CompilerProperties.Fragments.ImplicitAndExplicitNotAllowed;
import static com.sun.tools.javac.resources.CompilerProperties.Fragments.VarAndExplicitNotAllowed;
@ -5019,13 +5020,17 @@ public class JavacParser implements Parser {
// Parsing formalParameters sets the receiverParam, if present
List<JCVariableDecl> params = List.nil();
List<JCExpression> thrown = List.nil();
boolean unclosedParameterList;
if (!isRecord || name != names.init || token.kind == LPAREN) {
params = formalParameters();
unclosedParameterList = token.pos == endPosTable.errorEndPos;
if (!isVoid) type = bracketsOpt(type);
if (token.kind == THROWS) {
nextToken();
thrown = qualidentList(true);
}
} else {
unclosedParameterList = false;
}
saveDanglingDocComments(dc);
@ -5039,14 +5044,18 @@ public class JavacParser implements Parser {
if (token.kind == DEFAULT) {
accept(DEFAULT);
defaultValue = annotationValue();
accept(SEMI);
} else {
defaultValue = null;
accept(SEMI, tk -> Errors.Expected2(LBRACE, SEMI));
}
accept(SEMI);
if (token.pos <= endPosTable.errorEndPos) {
// error recovery
skip(false, true, false, false);
if (token.kind == LBRACE) {
// look if there is a probable missing opening brace,
// and if yes, parse as a block
boolean parseAsBlock = openingBraceMissing(unclosedParameterList);
if (parseAsBlock) {
body = block();
}
}
@ -5063,6 +5072,84 @@ public class JavacParser implements Parser {
}
}
/**
* After seeing a method header, and not seeing an opening left brace,
* attempt to estimate if acting as if the left brace was present and
* parsing the upcoming code will get better results than not parsing
* the code as a block.
*
* The estimate is as follows:
* - tokens are skipped until member, statement or identifier is found,
* - then, if there is a left brace, parse as a block,
* - otherwise, if the head was broken, do not parse as a block,
* - otherwise, look at the next token and:
* - if it definitelly starts a statement, parse as a block,
* - otherwise, if it is a closing/right brace, count opening and closing
* braces in the rest of the file, to see if imaginarily "adding" an opening
* brace would lead to a balanced count - if yes, parse as a block,
* - otherwise, speculatively parse the following code as a block, and if
* it contains statements that cannot be members, parse as a block,
* - otherwise, don't parse as a block.
*
* @param unclosedParameterList whether there was a serious problem in the
* parameters list
* @return true if and only if the following code should be parsed as a block.
*/
private boolean openingBraceMissing(boolean unclosedParameterList) {
skip(false, true, !unclosedParameterList, !unclosedParameterList);
if (token.kind == LBRACE) {
return true;
} else if (unclosedParameterList) {
return false;
} else {
return switch (token.kind) {
//definitelly sees a statement:
case CASE, DEFAULT, IF, FOR, WHILE, DO, TRY, SWITCH,
RETURN, THROW, BREAK, CONTINUE, ELSE, FINALLY,
CATCH, THIS, SUPER, NEW -> true;
case RBRACE -> {
//check if adding an opening brace would balance out
//the opening and closing braces:
int braceBalance = 1;
VirtualScanner virtualScanner = new VirtualScanner(S);
virtualScanner.nextToken();
while (virtualScanner.token().kind != TokenKind.EOF) {
switch (virtualScanner.token().kind) {
case LBRACE -> braceBalance++;
case RBRACE -> braceBalance--;
}
virtualScanner.nextToken();
}
yield braceBalance == 0;
}
default -> {
//speculatively try to parse as a block, and check
//if the result would suggest there is a block
//e.g.: it contains a statement that is not
//a member declaration
JavacParser speculative = new VirtualParser(this);
JCBlock speculativeResult =
speculative.block();
if (!speculativeResult.stats.isEmpty()) {
JCStatement last = speculativeResult.stats.last();
yield !speculativeResult.stats.stream().allMatch(s -> s.hasTag(VARDEF) ||
s.hasTag(CLASSDEF) ||
s.hasTag(BLOCK) ||
s == last) ||
!(last instanceof JCExpressionStatement exprStatement &&
exprStatement.expr.hasTag(ERRONEOUS));
} else {
yield false;
}
}
};
}
}
/** QualidentList = [Annotations] Qualident {"," [Annotations] Qualident}
*/
List<JCExpression> qualidentList(boolean allowAnnos) {

View File

@ -23,7 +23,7 @@
/*
* @test
* @bug 7073631 7159445 7156633 8028235 8065753 8205418 8205913 8228451 8237041 8253584 8246774 8256411 8256149 8259050 8266436 8267221 8271928 8275097 8293897 8295401 8304671 8310326 8312093 8312204 8315452 8337976
* @bug 7073631 7159445 7156633 8028235 8065753 8205418 8205913 8228451 8237041 8253584 8246774 8256411 8256149 8259050 8266436 8267221 8271928 8275097 8293897 8295401 8304671 8310326 8312093 8312204 8315452 8337976 8324859
* @summary tests error and diagnostics positions
* @author Jan Lahoda
* @modules jdk.compiler/com.sun.tools.javac.api
@ -2483,6 +2483,537 @@ public class JavacParserTest extends TestCase {
codes);
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion1() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
return true;
}
public static boolean test2() {
return true;
}
}""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion1: " + codes,
List.of("3:33:compiler.err.expected2"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test() {
return true;
}
\n\
public static boolean test2() {
return true;
}
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion2() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
public static boolean test2() {
return true;
}
} """;
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion2: " + codes,
List.of("3:33:compiler.err.expected2"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test();
\n\
public static boolean test2() {
return true;
}
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion3() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
}
""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion3: " + codes,
List.of("3:33:compiler.err.expected2"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test();
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion4() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
}
}
""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion4: " + codes,
List.of("3:33:compiler.err.expected2"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test() {
}
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion5() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test(String,
}
class T {}
""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion5: " + codes,
List.of("3:38:compiler.err.expected",
"4:1:compiler.err.illegal.start.of.type"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test(String <error>, (ERROR: ) <error>);
}
class T {
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion6() throws IOException {
String code = """
package tests;
public class TestB {
private Object testMethod(final String arg1 final String arg2) {
return null;
}
}
""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion5: " + codes,
List.of("3:48:compiler.err.expected3",
"3:66:compiler.err.expected"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
private Object testMethod(final String arg1);
final String arg2;
{
return null;
}
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion7() throws IOException {
//after 'default' attribute value, only semicolon (';') is expected,
//not left brace ('{'):
String code = """
package tests;
public @interface A {
public String value() default ""
}
""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion5: " + codes,
List.of("3:37:compiler.err.expected"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public @interface A {
\n\
public String value() default "";
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion10() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
String s = "";
return s.isEmpty();
}
public static boolean test2() {
return true;
}
}""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion1: " + codes,
List.of("3:33:compiler.err.expected2"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
public class TestB {
\n\
public static boolean test() {
String s = "";
return s.isEmpty();
}
\n\
public static boolean test2() {
return true;
}
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion11() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
String s = ""; //field declaration
public static boolean test2() {
return true;
}
}""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion1: " + codes,
List.of("3:33:compiler.err.expected2"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test();
String s = "";
\n\
public static boolean test2() {
return true;
}
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion12() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
final String s = "";
return s.isEmpty();
}
public static boolean test2() {
return true;
}
}""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion1: " + codes,
List.of("3:33:compiler.err.expected2"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test() {
final String s = "";
return s.isEmpty();
}
\n\
public static boolean test2() {
return true;
}
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion13() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
final String s = ""; //field declaration?
public static boolean test2() {
return true;
}
}""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion1: " + codes,
List.of("3:33:compiler.err.expected2"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test();
final String s = "";
\n\
public static boolean test2() {
return true;
}
}""");
}
@Test //JDK-8324859
void testImplicitlyDeclaredClassesConfusion14() throws IOException {
String code = """
package tests;
public class TestB {
public static boolean test() // missing open brace
String s = "";
s.length();
if (true); //force parse as block
public static boolean test2() {
return true;
}
}""";
DiagnosticCollector<JavaFileObject> coll =
new DiagnosticCollector<>();
JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, coll,
List.of("--enable-preview", "--source", SOURCE_VERSION),
null, Arrays.asList(new MyFileObject(code)));
CompilationUnitTree cut = ct.parse().iterator().next();
List<String> codes = new LinkedList<>();
for (Diagnostic<? extends JavaFileObject> d : coll.getDiagnostics()) {
codes.add(d.getLineNumber() + ":" + d.getColumnNumber() + ":" + d.getCode());
}
assertEquals("testImplicitlyDeclaredClassesConfusion1: " + codes,
List.of("3:33:compiler.err.expected2",
"7:5:compiler.err.illegal.start.of.expr"),
codes);
String result = toStringWithErrors(cut).replaceAll("\\R", "\n");
System.out.println("RESULT\n" + result);
assertEquals("incorrect AST",
result,
"""
package tests;
\n\
public class TestB {
\n\
public static boolean test() {
String s = "";
s.length();
if (true) ;
(ERROR: );
}
\n\
public static boolean test2() {
return true;
}
}""");
}
void run(String[] args) throws Exception {
int passed = 0, failed = 0;
final Pattern p = (args != null && args.length > 0)