Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,12 @@

this.lexer = new GroovyLangLexer(charStream);
this.parser = new GroovyLangParser(new CommonTokenStream(this.lexer));
this.parser.setErrorHandler(new DescriptiveErrorStrategy(charStream));

// Opt-in recovery (CompilerConfiguration.ERROR_RECOVERY): resync after syntax
// errors so IDEs can collect multiple diagnostics in one pass. Default is fail-fast.
// Multi-error truth for hosts is ErrorCollector; recovery may still yield a partial tree.
this.errorRecovery = sourceUnit.getConfiguration().isErrorRecoveryEnabled();
this.parser.setErrorHandler(DescriptiveErrorStrategy.create(charStream, this.errorRecovery));

this.groovydocManager = new GroovydocManager(groovydocEnabled, runtimeGroovydocEnabled);
this.tryWithResourcesASTTransformation = new TryWithResourcesASTTransformation(this);
Expand Down Expand Up @@ -216,7 +221,12 @@
AtnManager.READ_LOCK.lock();
try {
final TokenStream tokenStream = parser.getInputStream();
if (SLL_THRESHOLD >= 0 && tokenStream.size() > SLL_THRESHOLD) {
if (errorRecovery) {
// Recovery needs LL + listeners so every syntax error is reported.
// Skip SLL: recovering under SLL can "succeed" with a degraded tree
// and never re-run LL.
result = buildCST(PredictionMode.LL);
} else if (SLL_THRESHOLD >= 0 && tokenStream.size() > SLL_THRESHOLD) {
// The more tokens to parse, the more possibility SLL will fail and the more parsing time will waste.
// The option `groovy.antlr4.sll.threshold` could be tuned for better parsing performance, but it is disabled by default.
// If the token count is greater than `groovy.antlr4.sll.threshold`, use LL directly.
Expand Down Expand Up @@ -274,10 +284,32 @@
try {
return (ModuleNode) this.visit(this.buildCST());
} catch (Throwable t) {
throw convertException(t);
// convertException is a no-op for an existing CompilationFailedException and
// otherwise records (recovery) or fatally fails (default) before wrapping.
// Under recovery, return the partial module only when diagnostics were actually
// recorded — an empty collector with a CFE would otherwise look like silent success
// (belt-and-braces guard suggested in PR review).
CompilationFailedException cfe = convertException(t);
if (shouldReturnPartialModuleOnFailure()) {
return this.moduleNode;
}
throw cfe;
}
}

/**
* Whether recovery should return the partially built {@link #moduleNode} after a
* {@link CompilationFailedException} instead of rethrowing.
* <p>
* Requires both recovery mode and at least one recorded diagnostic. An empty
* collector with recovery enabled is treated as a real failure (rethrow) so a
* programming error in an error path cannot surface as silent success.
* </p>
*/
private boolean shouldReturnPartialModuleOnFailure() {
return errorRecovery && sourceUnit.getErrorCollector().hasErrors();
}

@Override
public ModuleNode visitCompilationUnit(final CompilationUnitContext ctx) {
this.visit(ctx.packageDeclaration());
Expand Down Expand Up @@ -3395,7 +3427,7 @@

throw createParsingFailedException("The LHS of an assignment should be a variable or a field accessing expression", ctx);
}

Check warning on line 3430 in src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 70 to 64, Complexity from 17 to 14, Nesting Level from 3 to 2, Number of Variables from 24 to 6.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ960OVxMvoxo5ITeFwW&open=AZ960OVxMvoxo5ITeFwW&pullRequest=2724
return configureAST(
new BinaryExpression(
leftExpr,
Expand Down Expand Up @@ -4874,7 +4906,6 @@
if (t instanceof SyntaxException) {
this.collectSyntaxError((SyntaxException) t);
} else if (t instanceof GroovySyntaxError groovySyntaxError) {

this.collectSyntaxError(
new SyntaxException(
groovySyntaxError.getMessage(),
Expand All @@ -4892,7 +4923,14 @@
}

private void collectSyntaxError(final SyntaxException e) {
sourceUnit.getErrorCollector().addFatalError(new SyntaxErrorMessage(e, sourceUnit));
SyntaxErrorMessage message = new SyntaxErrorMessage(e, sourceUnit);
if (errorRecovery) {
// Accumulate diagnostics so ANTLR resync can surface further errors in one pass.
// Fail-fast still uses addFatalError so the first error stops compilation immediately.
sourceUnit.getErrorCollector().addErrorAndContinue(message);
} else {
sourceUnit.getErrorCollector().addFatalError(message);
}
}

private void collectException(final Exception e) {
Expand Down Expand Up @@ -4968,6 +5006,8 @@
private final GroovyLangParser parser;
private final GroovydocManager groovydocManager;
private final TryWithResourcesASTTransformation tryWithResourcesASTTransformation;
/** {@code true} when {@link org.codehaus.groovy.control.CompilerConfiguration#ERROR_RECOVERY} is enabled. */
private final boolean errorRecovery;

private final List<ClassNode> classNodeList = new ArrayList<>();
private final Deque<ClassNode> classNodeStack = new ArrayDeque<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.groovy.parser.antlr4.internal;

import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.DefaultErrorStrategy;
import org.antlr.v4.runtime.FailedPredicateException;
import org.antlr.v4.runtime.InputMismatchException;
import org.antlr.v4.runtime.NoViableAltException;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.misc.Interval;

import java.util.Objects;

/**
* Shared friendly recognition diagnostics for Parrot error strategies.
* <p>
* Subclasses choose control flow ({@link DescriptiveErrorStrategy} fail-fast vs
* {@link RecoveringDescriptiveErrorStrategy} multi-error resync). Reporting —
* including {@link MissingDelimiterDiagnostic} — stays here so both modes share
* one message path.
* </p>
*/
abstract class AbstractFriendlyErrorStrategy extends DefaultErrorStrategy {

private final CharStream charStream;

AbstractFriendlyErrorStrategy(final CharStream charStream) {
this.charStream = charStream;
}

/**
* Prefer a precise "Missing …" delimiter diagnostic when the token stream
* clearly indicates an unclosed / incomplete construct; otherwise fall
* back to the generic message.
*/
private void reportFriendlyError(final Parser recognizer, final RecognitionException e, final String fallbackMessage) {
MissingDelimiterDiagnostic.Hit hit = null;
try {
// Incomplete / synthetic contexts can leave token indices out of range.
hit = MissingDelimiterDiagnostic.locate(recognizer.getInputStream(), e);
} catch (IndexOutOfBoundsException | IllegalArgumentException ignored) {
// Fall through to the generic message. Catch only locate()'s known
// defensive failures — never listener-side fatals (e.g. addFatalError).
}
if (hit != null) {
recognizer.notifyErrorListeners(hit.at, hit.message, e);
return;
}
notifyErrorListeners(recognizer, fallbackMessage, e);
}

protected String createNoViableAlternativeErrorMessage(final Parser recognizer, final NoViableAltException e) {
TokenStream tokens = recognizer.getInputStream();
String input;
if (tokens != null) {
if (e.getStartToken().getType() == Token.EOF) {
input = "<EOF>";
} else {
input = charStream.getText(Interval.of(e.getStartToken().getStartIndex(), e.getOffendingToken().getStopIndex()));
}
} else {
input = "<unknown input>";
}

return "Unexpected input: " + escapeWSAndQuote(input);
}

@Override
protected void reportNoViableAlternative(final Parser recognizer, final NoViableAltException e) {
reportFriendlyError(recognizer, e, createNoViableAlternativeErrorMessage(recognizer, e));
}

protected String createInputMismatchErrorMessage(final Parser recognizer, final InputMismatchException e) {
return "Unexpected input: " + getTokenErrorDisplay(e.getOffendingToken(recognizer));
}

@Override
protected void reportInputMismatch(final Parser recognizer, final InputMismatchException e) {
reportFriendlyError(recognizer, e, createInputMismatchErrorMessage(recognizer, e));
}

/**
* Format a failed-predicate diagnostic.
* <p>
* {@code recognizer} is part of the protected hook surface (parity with
* {@link #createNoViableAlternativeErrorMessage} /
* {@link #createInputMismatchErrorMessage}) so subclasses can include
* parser state when customising the message.
* </p>
*/
protected String createFailedPredicateErrorMessage(final Parser recognizer, final FailedPredicateException e) {
// Non-null contract only; default message is already complete on the exception.
Objects.requireNonNull(recognizer, "recognizer");
return e.getMessage();
}

@Override
protected void reportFailedPredicate(final Parser recognizer, final FailedPredicateException e) {
notifyErrorListeners(recognizer, createFailedPredicateErrorMessage(recognizer, e), e);
}
}
Loading
Loading