Java: better error recovery on annotation parsing (IDEA-209573)

GitOrigin-RevId: a600121ecbd9fa6338237f0d3802486c7f8a5656
This commit is contained in:
Bas Leijdekkers
2024-07-30 22:37:25 +00:00
committed by intellij-monorepo-bot
parent 0274e2096d
commit 1bdb332af2
9 changed files with 71 additions and 69 deletions
@@ -13,6 +13,7 @@ import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.QuickFixFactory;
import com.intellij.codeInsight.intention.impl.BaseIntentionAction;
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
import com.intellij.core.JavaPsiBundle;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.openapi.diagnostic.Logger;
@@ -47,6 +48,21 @@ public final class AnnotationsHighlightUtil {
private static final Logger LOG = Logger.getInstance(AnnotationsHighlightUtil.class);
static HighlightInfo.Builder checkNameValuePair(@NotNull PsiNameValuePair pair) {
if (pair.getFirstChild() instanceof PsiErrorElement) return null;
PsiIdentifier identifier = pair.getNameIdentifier();
if (identifier == null && pair.getParent() instanceof PsiAnnotationParameterList list) {
PsiNameValuePair[] attributes = list.getAttributes();
if (attributes.length > 1) {
String message = JavaPsiBundle.message("annotation.name.is.missing");
HighlightInfo.Builder highlightInfo = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR)
.range(pair)
.descriptionAndTooltip(message);
for (IntentionAction action : QuickFixFactory.getInstance().createAddAnnotationAttributeNameFixes(pair)) {
highlightInfo.registerFix(action, null, null, null, null);
}
return highlightInfo;
}
}
PsiAnnotation annotation = PsiTreeUtil.getParentOfType(pair, PsiAnnotation.class);
if (annotation == null) return null;
PsiClass annotationClass = annotation.resolveAnnotationType();
@@ -1,5 +1,5 @@
0.is.not.an.identifier=''{0}'' is not an identifier.
annotation.name.is.missing=Annotation attribute must be of the form 'name=value'
annotation.name.is.missing=Annotation attribute of the form 'name=value' expected
implicit.class.context.display=Implicit class
anonymous.class.context.display=Anonymous in {0}
anonymous.class.derived.display=Anonymous class derived from {0}
@@ -154,6 +154,7 @@ expected.module.declaration=Module declaration expected
expected.module.statement=Module directive expected
expected.package.reference=Package reference expected
expected.parameter=Parameter expected
expected.parameter.or.rparen=Annotation attribute or ')' expected
expected.rbrace='}' expected
expected.rbracket=']' expected
expected.resource=Resource definition expected
@@ -492,8 +492,8 @@ public class BasicDeclarationParser {
myParser.getReferenceParser()
.parseReferenceList(builder, JavaTokenType.THROWS_KEYWORD, myJavaElementTypeContainer.THROWS_LIST, JavaTokenType.COMMA);
if (anno && expect(builder, JavaTokenType.DEFAULT_KEYWORD)) {
parseAnnotationValue(builder);
if (anno && expect(builder, JavaTokenType.DEFAULT_KEYWORD) && parseAnnotationValue(builder) == null) {
error(builder, JavaPsiBundle.message("expected.value"));
}
return parseMethodBody(builder, declaration, anno);
@@ -935,107 +935,85 @@ public class BasicDeclarationParser {
private void parseAnnotationParameterList(final PsiBuilder builder) {
PsiBuilder.Marker list = builder.mark();
if (!expect(builder, JavaTokenType.LPARENTH)) {
if (!expect(builder, JavaTokenType.LPARENTH) || expect(builder, JavaTokenType.RPARENTH)) {
done(list, myJavaElementTypeContainer.ANNOTATION_PARAMETER_LIST, myWhiteSpaceAndCommentSetHolder);
return;
}
if (expect(builder, JavaTokenType.RPARENTH)) {
if (builder.getTokenType() == null) {
error(builder, JavaPsiBundle.message("expected.parameter.or.rparen"));
done(list, myJavaElementTypeContainer.ANNOTATION_PARAMETER_LIST, myWhiteSpaceAndCommentSetHolder);
return;
}
final boolean isFirstParamNamed = parseAnnotationParameter(builder, true);
boolean isFirstParamWarned = false;
boolean afterBad = false;
PsiBuilder.Marker elementMarker = parseAnnotationElement(builder);
while (true) {
final IElementType tokenType = builder.getTokenType();
IElementType tokenType = builder.getTokenType();
if (tokenType == null) {
error(builder, JavaPsiBundle.message("expected.parameter"));
error(builder, JavaPsiBundle.message(elementMarker == null ? "expected.parameter.or.rparen" : "expected.comma.or.rparen"));
break;
}
else if (expect(builder, JavaTokenType.RPARENTH)) {
break;
}
else if (tokenType == JavaTokenType.COMMA) {
final PsiBuilder.Marker errorStart = builder.mark();
final PsiBuilder.Marker errorEnd = builder.mark();
builder.advanceLexer();
final boolean hasParamName = parseAnnotationParameter(builder, false);
if (!isFirstParamNamed && hasParamName && !isFirstParamWarned) {
errorStart.errorBefore(JavaPsiBundle.message("annotation.name.is.missing"), errorEnd);
isFirstParamWarned = true;
elementMarker = parseAnnotationElement(builder);
if (elementMarker == null) {
error(builder, JavaPsiBundle.message("annotation.name.is.missing"));
tokenType = builder.getTokenType();
if (tokenType != JavaTokenType.COMMA && tokenType != JavaTokenType.RPARENTH) {
break;
}
}
else {
errorStart.drop();
}
errorEnd.drop();
}
else if (!afterBad) {
error(builder, JavaPsiBundle.message("expected.comma.or.rparen"));
builder.advanceLexer();
afterBad = true;
}
else {
afterBad = false;
parseAnnotationParameter(builder, false);
error(builder, JavaPsiBundle.message(elementMarker == null ? "expected.parameter.or.rparen" : "expected.comma.or.rparen"));
tokenType = builder.lookAhead(1);
if (tokenType != JavaTokenType.COMMA && tokenType != JavaTokenType.RPARENTH) break;
builder.advanceLexer();
}
}
done(list, myJavaElementTypeContainer.ANNOTATION_PARAMETER_LIST, myWhiteSpaceAndCommentSetHolder);
}
private boolean parseAnnotationParameter(final PsiBuilder builder, final boolean mayBeSimple) {
private PsiBuilder.Marker parseAnnotationElement(final PsiBuilder builder) {
PsiBuilder.Marker pair = builder.mark();
if (mayBeSimple) {
parseAnnotationValue(builder);
if (builder.getTokenType() != JavaTokenType.EQ) {
done(pair, myJavaElementTypeContainer.NAME_VALUE_PAIR, myWhiteSpaceAndCommentSetHolder);
return false;
}
pair.rollbackTo();
pair = builder.mark();
PsiBuilder.Marker valueMarker = parseAnnotationValue(builder);
if (valueMarker == null && builder.getTokenType() != JavaTokenType.EQ) {
pair.drop();
return null;
}
if (builder.getTokenType() != JavaTokenType.EQ) {
done(pair, myJavaElementTypeContainer.NAME_VALUE_PAIR, myWhiteSpaceAndCommentSetHolder);
return pair;
}
final boolean hasName = expectOrError(builder, JavaTokenType.IDENTIFIER, "expected.identifier");
pair.rollbackTo();
pair = builder.mark();
expectOrError(builder, JavaTokenType.EQ, "expected.eq");
parseAnnotationValue(builder);
expectOrError(builder, JavaTokenType.IDENTIFIER, "expected.identifier");
expect(builder, JavaTokenType.EQ);
valueMarker = parseAnnotationValue(builder);
if (valueMarker == null) error(builder, JavaPsiBundle.message("expected.value"));
done(pair, myJavaElementTypeContainer.NAME_VALUE_PAIR, myWhiteSpaceAndCommentSetHolder);
return hasName;
}
public void parseAnnotationValue(PsiBuilder builder) {
PsiBuilder.Marker result = doParseAnnotationValue(builder);
if (result == null) {
result = builder.mark();
result.error(JavaPsiBundle.message("expected.value"));
}
return pair;
}
@Nullable
private PsiBuilder.Marker doParseAnnotationValue(PsiBuilder builder) {
PsiBuilder.Marker result;
public PsiBuilder.Marker parseAnnotationValue(PsiBuilder builder) {
IElementType tokenType = builder.getTokenType();
if (tokenType == JavaTokenType.AT) {
result = parseAnnotation(builder);
return parseAnnotation(builder);
}
else if (tokenType == JavaTokenType.LBRACE) {
result = myParser.getExpressionParser().parseArrayInitializer(
builder, myJavaElementTypeContainer.ANNOTATION_ARRAY_INITIALIZER, this::doParseAnnotationValue, "expected.value");
return myParser.getExpressionParser().parseArrayInitializer(
builder, myJavaElementTypeContainer.ANNOTATION_ARRAY_INITIALIZER, this::parseAnnotationValue, "expected.value");
}
else {
result = myParser.getExpressionParser().parseConditional(builder);
return myParser.getExpressionParser().parseConditional(builder);
}
return result;
}
}
@@ -96,7 +96,8 @@ public final class JavaCompletionContributor extends CompletionContributor imple
psiNameValuePair().withSuperParent(2, psiElement(PsiAnnotation.class));
private static final ElementPattern<PsiElement> ANNOTATION_ATTRIBUTE_NAME =
or(psiElement(PsiIdentifier.class).withParent(NAME_VALUE_PAIR),
psiElement().afterLeaf("(").withParent(psiReferenceExpression().withParent(NAME_VALUE_PAIR)));
psiElement().afterLeaf("(").withParent(psiReferenceExpression().withParent(NAME_VALUE_PAIR)),
psiElement().afterLeaf(",").withParent(psiReferenceExpression().withParent(NAME_VALUE_PAIR)));
private static final PsiJavaElementPattern.Capture<PsiElement> IN_TYPE_PARAMETER =
psiElement().afterLeaf(PsiKeyword.EXTENDS, PsiKeyword.SUPER, "&").withParent(
psiElement(PsiReferenceList.class).withParent(PsiTypeParameter.class));
@@ -0,0 +1,9 @@
class Foo {
@<error descr="Cannot resolve symbol 'Deprec'">Deprec</error>(<error descr="Cannot resolve symbol 'ated'">ated</error><EOLError descr="',' or ')' expected"></EOLError>
void a() {}
void b() {}
void c() {}
}
@@ -5,7 +5,7 @@
@Ann(<error descr="Cannot find method 'value'">0</error>) class D {
}
@In(""<error descr="Annotation attribute must be of the form 'name=value'">,</error> create = "")
@In(<error descr="Annotation attribute of the form 'name=value' expected">""</error>, create = "")
class ZZZ {
}
@interface In {
@@ -12,8 +12,6 @@ PsiJavaFile:FirstNameMissed.java
PsiReferenceParameterList
<empty list>
PsiIdentifier:value1('value1')
PsiErrorElement:Annotation attribute must be of the form 'name=value'
<empty list>
PsiJavaToken:COMMA(',')
PsiWhiteSpace(' ')
PsiNameValuePair
@@ -12,8 +12,6 @@ test.java.file
REFERENCE_PARAMETER_LIST
<empty list>
IDENTIFIER
ERROR_ELEMENT
<empty list>
COMMA
WHITE_SPACE
NAME_VALUE_PAIR
@@ -38,6 +38,7 @@ public class AnnotationsHighlightingTest extends LightDaemonAnalyzerTestCase {
public void testReceiverParameters() { doTest(); }
public void testAnnotationOverIncompleteCode() { doTest(); }
public void testDeclarations() { doTest(); }
public void testErrorRecovery() { doTest(); }
private void doTest() { doTest(getTestName(true) + ".java"); }
private void doTest(String name) { doTest(BASE_PATH + "/" + name, false, false); }