Skip to content

Commit 5bfecf7

Browse files
SONARJAVA-5802 New analysis parameter: sonar.java.failOnStackOverflow (true by default) (#5323)
1 parent 2549360 commit 5bfecf7

File tree

5 files changed

+114
-8
lines changed

5 files changed

+114
-8
lines changed

java-frontend/src/main/java/org/sonar/java/SonarComponents.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ public class SonarComponents extends CheckRegistrar.RegistrarContext {
102102
* relying on (transitive) dependencies that do not respect modularization as defined by the JLS.
103103
*/
104104
public static final String SONAR_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE = "sonar.java.ignoreUnnamedModuleForSplitPackage";
105+
106+
/**
107+
* Describes whether the analysis should fail and halt when a StackOverflowError is encountered.
108+
* True by default, we still want to give the possibility to disable this behavior in case of large projects
109+
* for which it is costly to run a whole analysis to then just fail at the end and lose all findings.
110+
*/
111+
public static final String SONAR_FAIL_ON_STACKOVERFLOW = "sonar.java.internal.failOnStackOverflow";
112+
105113
private static final Version SONARLINT_6_3 = Version.parse("6.3");
106114
private static final Version SONARQUBE_9_2 = Version.parse("9.2");
107115
@VisibleForTesting
@@ -492,6 +500,10 @@ public boolean shouldIgnoreUnnamedModuleForSplitPackage() {
492500
return context.config().getBoolean(SONAR_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE).orElse(false);
493501
}
494502

503+
public boolean shouldFailOnStackOverflow() {
504+
return context.config().getBoolean(SONAR_FAIL_ON_STACKOVERFLOW).orElse(true);
505+
}
506+
495507
private static long computeIdealBatchSize() {
496508
// We take a fraction of the total memory available though -Xmx.
497509
// If we assume that the average size of a file is 5KB and the average CI should have 1GB of memory,

java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ public void simpleScan(InputFile inputFile, JParserConfig.Result result, Consume
187187
interruptIfFailFast(e, inputFile);
188188
} catch (StackOverflowError error) {
189189
LOG.error(String.format(LOG_ERROR_STACKOVERFLOW, inputFile), error);
190-
throw error;
190+
if (sonarComponents == null || sonarComponents.shouldFailOnStackOverflow()) {
191+
throw error;
192+
}
191193
} finally {
192194
telemetry.aggregateAsCounter(telemetryAnalysisKeys.sizeCharsKey(), InputFileUtils.charCount(inputFile, 0));
193195
telemetry.aggregateAsCounter(telemetryAnalysisKeys.timeMsKey(), currentTimeMillis() - startTime);

java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.junit.jupiter.params.provider.Arguments;
4242
import org.junit.jupiter.params.provider.CsvSource;
4343
import org.junit.jupiter.params.provider.MethodSource;
44+
import org.junit.jupiter.params.provider.ValueSource;
4445
import org.mockito.Mock;
4546
import org.mockito.Mockito;
4647
import org.mockito.junit.jupiter.MockitoExtension;
@@ -1064,6 +1065,23 @@ void shouldIgnoreUnnamedModuleForSplitPackage_returns_true_when_analysis_paramet
10641065
assertThat(sonarComponents.shouldIgnoreUnnamedModuleForSplitPackage()).isTrue();
10651066
}
10661067

1068+
@Test
1069+
void shouldFailOnStackOverflow_returns_true_by_default() {
1070+
MapSettings settings = new MapSettings();
1071+
SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null);
1072+
sonarComponents.setSensorContext(SensorContextTester.create(new File("")).setSettings(settings));
1073+
assertThat(sonarComponents.shouldFailOnStackOverflow()).isTrue();
1074+
}
1075+
1076+
@ParameterizedTest
1077+
@ValueSource(strings = {"true", "false"})
1078+
void shouldFailOnStackOverflow_returns_correct_value_when_set(String value) {
1079+
MapSettings settings = new MapSettings().setProperty("sonar.java.internal.failOnStackOverflow", value);
1080+
SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null);
1081+
sonarComponents.setSensorContext(SensorContextTester.create(new File("")).setSettings(settings));
1082+
assertThat(sonarComponents.shouldFailOnStackOverflow()).isEqualTo(Boolean.valueOf(value));
1083+
}
1084+
10671085
@Nested
10681086
class Logging {
10691087
private final DecimalFormat formatter = new DecimalFormat("00");

java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.junit.jupiter.api.Test;
3030
import org.junit.jupiter.api.extension.RegisterExtension;
3131
import org.junit.jupiter.params.ParameterizedTest;
32+
import org.junit.jupiter.params.provider.MethodSource;
3233
import org.junit.jupiter.params.provider.ValueSource;
3334
import org.slf4j.event.Level;
3435
import org.sonar.api.batch.fs.InputFile;
@@ -67,6 +68,7 @@
6768
import static org.assertj.core.api.Assertions.assertThat;
6869
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6970
import static org.assertj.core.api.Assertions.fail;
71+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
7072
import static org.junit.jupiter.api.Assertions.assertThrows;
7173
import static org.mockito.ArgumentMatchers.any;
7274
import static org.mockito.Mockito.doReturn;
@@ -81,6 +83,11 @@
8183

8284
class JavaAstScannerTest {
8385

86+
public static final InputFile TRIVIAL_COMPILATION_UNIT = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt");
87+
public static final JavaFileScanner PROBLEMATIC_VISITOR = ctx -> {
88+
throw new StackOverflowError();
89+
};
90+
8491
@RegisterExtension
8592
public ThreadLocalLogTester logTester = new ThreadLocalLogTester().setLevel(Level.DEBUG);
8693

@@ -450,7 +457,6 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned
450457

451458
@Test
452459
void test_modifyCompilationUnit_modify_ast() {
453-
InputFile trivialCompilationUnit = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt");
454460

455461
var check = new JavaFileScanner() {
456462
@Override
@@ -468,7 +474,7 @@ public void scanFile(JavaFileScannerContext context) {
468474

469475
JavaAstScanner scanner = new JavaAstScanner(null, new NoOpTelemetry(), TelemetryKey.JAVA_ANALYSIS_MAIN);
470476
scanner.setVisitorBridge(visitorsBridge);
471-
scanner.scanForTesting(Collections.singletonList(trivialCompilationUnit), compilationUnit -> {
477+
scanner.scanForTesting(Collections.singletonList(TRIVIAL_COMPILATION_UNIT), compilationUnit -> {
472478
var clazz = (ClassTreeImpl) ((JavaTree.CompilationUnitTreeImpl) compilationUnit).types().get(0);
473479
assertThat(clazz.simpleName().name()).isEqualTo("C");
474480
assertThat(clazz.simpleName().symbol().isUnknown()).isFalse();
@@ -478,6 +484,59 @@ public void scanFile(JavaFileScannerContext context) {
478484

479485
}
480486

487+
@Test
488+
void test_should_fail_on_stackoverflow_by_default() {
489+
var files = List.of(TRIVIAL_COMPILATION_UNIT);
490+
List<JavaFileScanner> visitors = List.of(PROBLEMATIC_VISITOR);
491+
492+
// Assert that by default, StackOverflowError is propagated
493+
var emptySettings = new MapSettings();
494+
assertThrows(StackOverflowError.class, () -> scanFilesWithVisitorsAndContext(files, visitors, emptySettings));
495+
}
496+
497+
@Test
498+
void test_should_not_fail_on_stackoverflow_when_set() {
499+
// Assert that when configured to not fail on exception, StackOverflowError is swallowed, but logged
500+
MapSettings settings = new MapSettings().setProperty(SonarComponents.SONAR_FAIL_ON_STACKOVERFLOW, false);
501+
assertDoesNotThrow(() -> scanFilesWithVisitorsAndContext(List.of(TRIVIAL_COMPILATION_UNIT), List.of(PROBLEMATIC_VISITOR), settings));
502+
assertThat(logTester.logs(Level.ERROR))
503+
.containsExactly("A stack overflow error occurred while analyzing file: 'src/test/resources/AstScannerNoParseError.txt'");
504+
}
505+
506+
@ParameterizedTest
507+
@MethodSource("failFastAndFailOnStackOverflowCombinations")
508+
void assertFailFastWithFailOnStackOverflowInteraction(boolean failFast, boolean failOnSO, boolean shouldFail) {
509+
MapSettings settings = new MapSettings()
510+
.setProperty(SonarComponents.FAIL_ON_EXCEPTION_KEY, failFast)
511+
.setProperty(SonarComponents.SONAR_FAIL_ON_STACKOVERFLOW, failOnSO);
512+
var files = List.of(TRIVIAL_COMPILATION_UNIT);
513+
List<JavaFileScanner> visitors = List.of(PROBLEMATIC_VISITOR);
514+
515+
if (shouldFail) {
516+
assertThrows(StackOverflowError.class, () -> scanFilesWithVisitorsAndContext(files, visitors, settings));
517+
} else {
518+
assertDoesNotThrow(() -> scanFilesWithVisitorsAndContext(files, visitors, settings));
519+
}
520+
}
521+
522+
static List<Object[]> failFastAndFailOnStackOverflowCombinations() {
523+
return Arrays.asList(new Object[][]{
524+
{true, true, true},
525+
{true, false, false},
526+
{false, true, true},
527+
{false, false, false}
528+
});
529+
}
530+
531+
@Test
532+
void test_should_fail_on_stackoverflow_when_sonar_components_null() {
533+
var files = List.of(TRIVIAL_COMPILATION_UNIT);
534+
JavaAstScanner scanner = new JavaAstScanner(null, new NoOpTelemetry(), TelemetryKey.JAVA_ANALYSIS_MAIN);
535+
VisitorsBridge visitorBridge = new VisitorsBridge(PROBLEMATIC_VISITOR);
536+
scanner.setVisitorBridge(visitorBridge);
537+
assertThrows(StackOverflowError.class, () -> scanner.scan(files));
538+
}
539+
481540
private void scanSingleFile(InputFile file, boolean failOnException) {
482541
scanFilesWithVisitors(Collections.singletonList(file), Collections.emptyList(), -1, failOnException, false);
483542
}
@@ -501,19 +560,27 @@ private void scanWithJavaVersion(int version, List<InputFile> inputFiles, List<J
501560
}
502561

503562
private void scanFilesWithVisitors(List<InputFile> inputFiles, List<JavaFileScanner> visitors,
504-
int javaVersion, boolean failOnException, boolean autoscanMode) {
505-
context.setSettings(new MapSettings()
563+
int javaVersion, boolean failOnException, boolean autoscanMode) {
564+
MapSettings settings = new MapSettings()
506565
.setProperty(SonarComponents.FAIL_ON_EXCEPTION_KEY, failOnException)
507-
.setProperty(SonarComponents.SONAR_AUTOSCAN, autoscanMode)
508-
);
566+
.setProperty(SonarComponents.SONAR_AUTOSCAN, autoscanMode);
567+
568+
scanFilesWithVisitorsAndContext(inputFiles, visitors, settings, javaVersion);
569+
}
570+
571+
private void scanFilesWithVisitorsAndContext(List<InputFile> inputFiles, List<JavaFileScanner> visitors, MapSettings contextMap) {
572+
scanFilesWithVisitorsAndContext(inputFiles, visitors, contextMap, JavaVersionImpl.MAX_SUPPORTED);
573+
}
509574

575+
private void scanFilesWithVisitorsAndContext(List<InputFile> inputFiles, List<JavaFileScanner> visitors, MapSettings contextMap, int javaVersion) {
576+
context.setSettings(contextMap);
510577
DefaultFileSystem fileSystem = context.fileSystem();
511578
ClasspathForMain classpathForMain = new ClasspathForMain(context.config(), fileSystem);
512579
ClasspathForTest classpathForTest = new ClasspathForTest(context.config(), fileSystem);
513580
SonarComponents sonarComponents = new SonarComponents(null, fileSystem, classpathForMain, classpathForTest, null, null);
514581
sonarComponents.setSensorContext(context);
515582
JavaAstScanner scanner = new JavaAstScanner(sonarComponents, new NoOpTelemetry(), TelemetryKey.JAVA_ANALYSIS_MAIN);
516-
VisitorsBridge visitorBridge = new VisitorsBridge(visitors, new ArrayList<>(), sonarComponents, new JavaVersionImpl(javaVersion));
583+
VisitorsBridge visitorBridge = new VisitorsBridge(visitors, List.of(), sonarComponents, new JavaVersionImpl(javaVersion));
517584
scanner.setVisitorBridge(visitorBridge);
518585
scanner.scan(inputFiles);
519586
}

sonar-java-plugin/src/main/resources/static/documentation.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ You can control this behavior with the analysis parameter `sonar.java.skipUnchan
101101
* setting it to `false` will **never** make rules skip unchanged files, even if the context is a PR analysis
102102
* not setting this parameter lets the server decide whether the optimization should be enabled, by default it will be enabled for PR analyses.
103103

104+
## Handling analysis runtime errors
105+
By default, the Java analyzer only halts the analysis when an `AnalysisException` or a `java.lang.Error` (non-recoverable error) is thrown.
106+
Generic runtime exceptions (like `NullPointerException`, `ArrayIndexOutOfBoundsException`, etc.) are caught and logged but do not stop the analysis.
107+
This behavior can be changed by setting two different analysis parameters:
108+
* `sonar.internal.analysis.failFast` - when set to `true`, any runtime exception will halt the analysis
109+
* `sonar.java.internal.failOnStackOverflow` - `true` by default, can be set to `false` to avoid halting the analysis on `StackOverflowError` (this overrides the `failFast` option)
110+
104111
## Cache-enabled rules (experimental)
105112
Starting from April 2022, the Java analyzer offers rule developers a SQ cache that can be used to store and retrieve information from one analysis to the other.
106113
The cache is provided by the underlying SonarQube instance and is branch-specific.

0 commit comments

Comments
 (0)