diff --git a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/ExpressionsHelperTest.java b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/ExpressionsHelperTest.java index ebc56962d25..a2e58209074 100644 --- a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/ExpressionsHelperTest.java +++ b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/ExpressionsHelperTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -37,6 +38,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped class ExpressionsHelperTest extends JParserTestUtils { @Test diff --git a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java index d0e1076e027..2c16a2321fe 100644 --- a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java +++ b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java @@ -24,11 +24,13 @@ import java.util.List; import java.util.stream.Collectors; -import org.sonar.java.model.JParser; +import org.sonar.java.model.FrontendParser; import org.sonar.java.model.JParserConfig; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.tree.*; +@TestResourceScope.Scoped public abstract class JParserTestUtils { static IdentifierTree variableFromLastReturnStatement(List statements) { @@ -77,7 +79,7 @@ public static CompilationUnitTree parse(File file) { public static CompilationUnitTree parse(String source) { List classpath = Arrays.asList(new File("target/test-classes"), new File("target/classes")); JavaVersion version = JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION; - return JParser.parse(JParserConfig.Mode.FILE_BY_FILE.create(version, classpath).astParser(), version.toString(), "test", source); + return TestResourceScope.retain(FrontendParser.parse(version, classpath, "test", source)).tree(); } } diff --git a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java index acf22c22f89..9f0205b51ff 100644 --- a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java +++ b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java @@ -20,6 +20,7 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.*; @@ -27,6 +28,7 @@ import static java.lang.reflect.Modifier.isPrivate; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class ReassignmentFinderTest extends JParserTestUtils { @Test diff --git a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/TreeHelperTest.java b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/TreeHelperTest.java index 056b5f5056b..f32e31f3def 100644 --- a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/TreeHelperTest.java +++ b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/TreeHelperTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.sonar.java.model.statement.BlockTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BlockTree; import org.sonar.plugins.java.api.tree.ExpressionStatementTree; import org.sonar.plugins.java.api.tree.ForEachStatement; @@ -36,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class TreeHelperTest extends JParserTestUtils { @Test diff --git a/java-checks-test-sources/test-classpath-reader/pom.xml b/java-checks-test-sources/test-classpath-reader/pom.xml index 1c3446bab4a..d8e1eb7cc28 100644 --- a/java-checks-test-sources/test-classpath-reader/pom.xml +++ b/java-checks-test-sources/test-classpath-reader/pom.xml @@ -14,6 +14,11 @@ Code Analyzer for Java :: Test Classpath Reader + + org.junit.jupiter + junit-jupiter-api + provided + com.google.code.findbugs jsr305 diff --git a/java-checks-test-sources/test-classpath-reader/src/main/java/org/sonar/java/test/TestResourceScope.java b/java-checks-test-sources/test-classpath-reader/src/main/java/org/sonar/java/test/TestResourceScope.java new file mode 100644 index 00000000000..2c8134d7163 --- /dev/null +++ b/java-checks-test-sources/test-classpath-reader/src/main/java/org/sonar/java/test/TestResourceScope.java @@ -0,0 +1,126 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayDeque; +import java.util.Deque; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** Retains lazy fixtures through instance initialization, nested tests and all assertions in a test class. */ +public final class TestResourceScope implements BeforeAllCallback, AfterAllCallback { + + private static final ThreadLocal> SCOPES = ThreadLocal.withInitial(ArrayDeque::new); + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(TestResourceScope.class); + + @Inherited + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @ExtendWith(TestResourceScope.class) + @Execution(ExecutionMode.SAME_THREAD) + public @interface Scoped { + } + + public static Scope open() { + Scope scope = new Scope(); + SCOPES.get().push(scope); + return scope; + } + + public static T retain(T resource) { + Scope scope = SCOPES.get().peek(); + if (scope == null) { + IllegalStateException failure = new IllegalStateException("Parser fixtures require @TestResourceScope.Scoped or an explicit scope"); + try { + resource.close(); + } catch (Exception cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + throw failure; + } + scope.resources.push(resource); + return resource; + } + + @Override + public void beforeAll(ExtensionContext context) { + context.getStore(NAMESPACE).put("scope", open()); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + Scope scope = context.getStore(NAMESPACE).remove("scope", Scope.class); + if (scope != null) { + scope.close(); + } + } + + public static final class Scope implements AutoCloseable { + private final Deque resources = new ArrayDeque<>(); + private boolean closed; + + private Scope() { + } + + /** Attempts every fixture even after errors, then rethrows the first failure with later failures suppressed. */ + @Override + public void close() throws Exception { + if (closed) { + return; + } + if (SCOPES.get().peek() != this) { + throw new IllegalStateException("Test resource scopes must close in reverse order on their owning thread"); + } + closed = true; + SCOPES.get().pop(); + if (SCOPES.get().isEmpty()) { + SCOPES.remove(); + } + Throwable failure = null; + try { + while (!resources.isEmpty()) { + try { + resources.pop().close(); + } catch (Exception | Error e) { + if (failure == null) { + failure = e; + } else if (failure != e) { + failure.addSuppressed(e); + } + } + } + } finally { + resources.clear(); + } + if (failure instanceof Error error) { + throw error; + } + if (failure instanceof Exception exception) { + throw exception; + } + } + } +} diff --git a/java-checks-test-sources/test-classpath-reader/src/test/java/org/sonar/java/test/TestResourceScopeTest.java b/java-checks-test-sources/test-classpath-reader/src/test/java/org/sonar/java/test/TestResourceScopeTest.java new file mode 100644 index 00000000000..fa0f4035063 --- /dev/null +++ b/java-checks-test-sources/test-classpath-reader/src/test/java/org/sonar/java/test/TestResourceScopeTest.java @@ -0,0 +1,114 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.test; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TestResourceScopeTest { + + @Test + void nested_scopes_close_only_their_own_resources_after_assertions() throws Exception { + List calls = new ArrayList<>(); + try (TestResourceScope.Scope outer = TestResourceScope.open()) { + TestResourceScope.retain(() -> calls.add("outer")); + try (TestResourceScope.Scope inner = TestResourceScope.open()) { + TestResourceScope.retain(() -> calls.add("inner")); + assertThat(calls).isEmpty(); + } + assertThat(calls).containsExactly("inner"); + } + assertThat(calls).containsExactly("inner", "outer"); + } + + @Test + void scope_closes_every_resource_in_reverse_order_even_when_cleanup_fails() throws Exception { + TestResourceScope.Scope scope = TestResourceScope.open(); + List calls = new ArrayList<>(); + IllegalStateException first = new IllegalStateException("first"); + Exception second = new Exception("second"); + TestResourceScope.retain(() -> calls.add("last")); + TestResourceScope.retain(() -> { calls.add("second"); throw second; }); + TestResourceScope.retain(() -> { calls.add("first"); throw first; }); + assertThatThrownBy(scope::close).isSameAs(first).hasSuppressedException(second); + scope.close(); + assertThat(calls).containsExactly("first", "second", "last"); + } + + @Test + void scope_cleanup_attempts_every_resource_and_suppresses_errors() throws Exception { + TestResourceScope.Scope scope = TestResourceScope.open(); + List calls = new ArrayList<>(); + AssertionError failure = new AssertionError("cleanup"); + TestResourceScope.retain(() -> calls.add("last")); + TestResourceScope.retain(() -> { throw failure; }); + Exception first = new Exception("first"); + TestResourceScope.retain(() -> { throw first; }); + + assertThatThrownBy(scope::close).isSameAs(first).hasSuppressedException(failure); + scope.close(); + assertThat(calls).containsExactly("last"); + try (TestResourceScope.Scope next = TestResourceScope.open()) { + TestResourceScope.retain(() -> calls.add("next")); + } + assertThat(calls).containsExactly("last", "next"); + } + + @Test + void scope_cleanup_rethrows_first_error_after_attempting_every_resource() throws Exception { + TestResourceScope.Scope scope = TestResourceScope.open(); + List calls = new ArrayList<>(); + AssertionError first = new AssertionError("first"); + Exception second = new Exception("second"); + TestResourceScope.retain(() -> calls.add("last")); + TestResourceScope.retain(() -> { throw second; }); + TestResourceScope.retain(() -> { throw first; }); + TestResourceScope.retain(() -> { throw first; }); + + assertThatThrownBy(scope::close).isSameAs(first).hasSuppressedException(second); + scope.close(); + assertThat(calls).containsExactly("last"); + } + + @Test + void missing_scope_fails_loudly_and_closes_the_unretained_fixture() { + List calls = new ArrayList<>(); + assertThatThrownBy(() -> TestResourceScope.retain(() -> calls.add("closed"))) + .isInstanceOf(IllegalStateException.class).hasMessageContaining("@TestResourceScope.Scoped"); + assertThat(calls).containsExactly("closed"); + } + + @Test + void missing_scope_preserves_cleanup_exceptions_as_suppressed() { + Exception failure = new Exception("cleanup failed"); + assertThatThrownBy(() -> TestResourceScope.retain(() -> { throw failure; })) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("@TestResourceScope.Scoped") + .hasSuppressedException(failure); + } + + @Test + void missing_scope_propagates_cleanup_errors() { + AssertionError failure = new AssertionError("cleanup failed"); + assertThatThrownBy(() -> TestResourceScope.retain(() -> { throw failure; })) + .isSameAs(failure); + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/ImportDeclarationOrderCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/ImportDeclarationOrderCheckTest.java index a464ed808d2..e7f47922a1d 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/ImportDeclarationOrderCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/ImportDeclarationOrderCheckTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.sonar.java.checks.helpers.JParserTestUtils; import org.sonar.java.checks.verifier.CheckVerifier; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ImportTree; import org.sonar.plugins.java.api.tree.Tree; @@ -32,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; +@TestResourceScope.Scoped class ImportDeclarationOrderCheckTest { @Test @DisplayName("No issues with properly ordered imports including modules") diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/ClassPatternsUtilsTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/ClassPatternsUtilsTest.java index e195948572a..95d5ad552ed 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/ClassPatternsUtilsTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/ClassPatternsUtilsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JavaVersionImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -25,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +@TestResourceScope.Scoped class ClassPatternsUtilsTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/DeprecatedCheckerHelperTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/DeprecatedCheckerHelperTest.java index a945a056ba3..3d790c4be84 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/DeprecatedCheckerHelperTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/DeprecatedCheckerHelperTest.java @@ -21,12 +21,14 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +@TestResourceScope.Scoped class DeprecatedCheckerHelperTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizerTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizerTest.java index 6a5cc4e18af..f2f55c829e4 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizerTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizerTest.java @@ -19,11 +19,13 @@ import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.LiteralTree; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class GeneratedStringLiteralRecognizerTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/IntegerOverflowRangeTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/IntegerOverflowRangeTest.java index 00abe83a526..6c63ff36ae7 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/IntegerOverflowRangeTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/IntegerOverflowRangeTest.java @@ -21,6 +21,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.checks.helpers.IntegerOverflowRange.Range; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ExpressionTree; @@ -28,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class IntegerOverflowRangeTest { private static final BigInteger INT_MIN = BigInteger.valueOf(Integer.MIN_VALUE); diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java index fd512787b13..6687f496c50 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java @@ -22,8 +22,9 @@ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; -import org.sonar.java.model.JParser; +import org.sonar.java.model.FrontendParser; import org.sonar.java.model.JParserConfig; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; import org.sonar.plugins.java.api.tree.ClassTree; @@ -37,6 +38,7 @@ import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.VariableTree; +@TestResourceScope.Scoped public abstract class JParserTestUtils { static IdentifierTree variableFromLastReturnStatement(List statements) { @@ -85,7 +87,7 @@ public static CompilationUnitTree parse(File file) { public static CompilationUnitTree parse(String source) { List classpath = Arrays.asList(new File("target/test-classes"), new File("target/classes")); JavaVersion version = JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION; - return JParser.parse(JParserConfig.Mode.FILE_BY_FILE.create(version, classpath).astParser(), version.toString(), "test", source); + return TestResourceScope.retain(FrontendParser.parse(version, classpath, "test", source)).tree(); } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/JavaPropertiesHelperTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/JavaPropertiesHelperTest.java index 59b8432ccd3..843c56e6ea3 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/JavaPropertiesHelperTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/JavaPropertiesHelperTest.java @@ -17,6 +17,7 @@ package org.sonar.java.checks.helpers; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ExpressionStatementTree; @@ -30,6 +31,7 @@ import java.lang.reflect.Constructor; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class JavaPropertiesHelperTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/JavadocTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/JavadocTest.java index a3dd9eee31e..b076e070a17 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/JavadocTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/JavadocTest.java @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.MethodTree; @@ -31,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class JavadocTest { private static CompilationUnitTree tree; private static Javadoc fooJavadoc; diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java index f1ced12fd27..ef8d9409e54 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JavaVersionImpl; import org.sonar.java.model.expression.LiteralTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; @@ -43,6 +44,7 @@ import static org.sonar.java.checks.helpers.MethodTreeUtils.lamdaArgumentAt; import static org.sonar.java.checks.helpers.MethodTreeUtils.parentMethodInvocationOfArgumentAtPos; +@TestResourceScope.Scoped class MethodTreeUtilsTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/NullabilityDataUtilsTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/NullabilityDataUtilsTest.java index 4633f3bb81c..24e5fc92dfa 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/NullabilityDataUtilsTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/NullabilityDataUtilsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JavaTree; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.VariableTree; @@ -25,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.java.checks.helpers.NullabilityDataUtils.nullabilityAsString; +@TestResourceScope.Scoped class NullabilityDataUtilsTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/QuickFixHelperTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/QuickFixHelperTest.java index 0deff7b18e9..98c85a7bb3e 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/QuickFixHelperTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/QuickFixHelperTest.java @@ -31,6 +31,7 @@ import org.sonar.java.model.InternalSyntaxToken; import org.sonar.java.reporting.JavaQuickFix; import org.sonar.java.reporting.JavaTextEdit; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ClassTree; @@ -48,6 +49,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped class QuickFixHelperTest { private static void requireSingleParameter(ParameterDeclarations parameterDeclarations) { diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java index 9c4d2fa995c..35a77dbac73 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.util.List; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.BlockTree; import org.sonar.plugins.java.api.tree.ClassTree; @@ -37,6 +38,7 @@ import static java.lang.reflect.Modifier.isPrivate; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class ReassignmentFinderTest extends JParserTestUtils { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/SpringContextModelTestUtils.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/SpringContextModelTestUtils.java index e5c715f9739..836eb360ad0 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/SpringContextModelTestUtils.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/SpringContextModelTestUtils.java @@ -28,7 +28,7 @@ import org.sonar.api.batch.fs.InputFile; import org.sonar.java.SonarComponents; import org.sonar.java.checks.verifier.TestUtils; -import org.sonar.java.model.JParser; +import org.sonar.java.model.FrontendParser; import org.sonar.java.model.JParserConfig; import org.sonar.java.model.VisitorsBridge; import org.sonar.java.model.springcontext.BeanDefinitionGatherer; @@ -44,7 +44,6 @@ import org.sonar.java.test.classpath.TestClasspathUtils; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.JavaVersion; -import org.sonar.plugins.java.api.tree.CompilationUnitTree; /** * Builds a {@link SpringContextModel} for the tests of rules that consume one instead of visiting an AST, either @@ -145,9 +144,10 @@ private static SpringContextModel buildModel(List filePaths) { VisitorsBridge visitorsBridge = new VisitorsBridge(gatherers, classpath, sonarComponents); for (String filePath : filePaths) { File file = new File(filePath); - CompilationUnitTree compilationUnit = parse(file, classpath); - visitorsBridge.setCurrentFile(inputFile(file)); - visitorsBridge.visitFile(compilationUnit, false); + try (FrontendParser.ParsedFile parsed = parse(file, classpath)) { + visitorsBridge.setCurrentFile(inputFile(file)); + visitorsBridge.visitFile(parsed.tree(), false); + } } visitorsBridge.endOfAnalysis(); return SpringContextModel.of(gatheringModel); @@ -166,7 +166,7 @@ private static InputFile inputFile(File file) { } } - private static CompilationUnitTree parse(File file, List classpath) { + private static FrontendParser.ParsedFile parse(File file, List classpath) { String source; try { source = Files.readString(file.toPath(), StandardCharsets.UTF_8); @@ -174,6 +174,6 @@ private static CompilationUnitTree parse(File file, List classpath) { throw new IllegalStateException("Unable to read file '" + file.getAbsolutePath() + "'", e); } JavaVersion version = JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION; - return JParser.parse(JParserConfig.Mode.FILE_BY_FILE.create(version, classpath).astParser(), version.toString(), file.getName(), source); + return FrontendParser.parse(version, classpath, file.getName(), source); } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/TryCatchUtilsTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/TryCatchUtilsTest.java index 12c542bb99e..24720c887b7 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/TryCatchUtilsTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/TryCatchUtilsTest.java @@ -17,6 +17,7 @@ package org.sonar.java.checks.helpers; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.MethodTree; @@ -25,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class TryCatchUtilsTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/ValueBasedUtilsTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/ValueBasedUtilsTest.java index f5f4f1238ef..24748927e06 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/ValueBasedUtilsTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/ValueBasedUtilsTest.java @@ -17,6 +17,7 @@ package org.sonar.java.checks.helpers; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.SyntaxTrivia; @@ -28,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class ValueBasedUtilsTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/regex/AbstractRegexCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/regex/AbstractRegexCheckTest.java index 33e9585c4d7..d5880868c97 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/regex/AbstractRegexCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/regex/AbstractRegexCheckTest.java @@ -27,6 +27,7 @@ import org.sonar.check.Rule; import org.sonar.java.checks.helpers.JParserTestUtils; import org.sonar.java.checks.verifier.CheckVerifier; +import org.sonar.java.test.TestResourceScope; import org.sonarsource.analyzer.commons.regex.RegexParseResult; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ClassTree; @@ -46,6 +47,7 @@ import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; import static org.sonar.java.checks.regex.AbstractRegexCheck.getLiterals; +@TestResourceScope.Scoped class AbstractRegexCheckTest { private static final String JAVA_CODE = """ diff --git a/java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java index 2927a17d7c0..55d7bd2bb0a 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/spring/AmbiguousDependencyCheckTest.java @@ -26,6 +26,7 @@ import org.sonar.java.model.springcontext.BeanDefinitionHolder; import org.sonar.java.model.springcontext.ProfileExpression; import org.sonar.java.model.springcontext.SpringContextModel; +import org.sonar.java.test.TestResourceScope; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.java.checks.helpers.SpringContextModelTestUtils.beanLocation; @@ -33,6 +34,7 @@ import static org.sonar.java.checks.helpers.SpringContextModelTestUtils.dummyInputFile; import static org.sonar.java.checks.helpers.SpringContextModelTestUtils.indexBeanByType; +@TestResourceScope.Scoped class AmbiguousDependencyCheckTest { private static final String BASE_PATH = "checks/spring/s9352/"; diff --git a/java-checks/src/test/java/org/sonar/java/checks/spring/SpringBeansShouldBeAccessibleCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/spring/SpringBeansShouldBeAccessibleCheckTest.java index 45ab07145bb..ee31751ee96 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/spring/SpringBeansShouldBeAccessibleCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/spring/SpringBeansShouldBeAccessibleCheckTest.java @@ -24,6 +24,7 @@ import org.sonar.java.model.springcontext.BeanLocation; import org.sonar.java.model.springcontext.SpringContextModel; import org.sonar.java.reporting.AnalyzerMessage; +import org.sonar.java.test.TestResourceScope; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.java.checks.helpers.SpringContextModelTestUtils.beanLocation; @@ -31,6 +32,7 @@ import static org.sonar.java.checks.helpers.SpringContextModelTestUtils.dummyInputFile; import static org.sonar.java.checks.helpers.SpringContextModelTestUtils.registerBean; +@TestResourceScope.Scoped class SpringBeansShouldBeAccessibleCheckTest { private static final String BASE_PATH = "checks/spring/s4605/"; diff --git a/java-checks/src/test/java/org/sonar/java/checks/tests/AssertionTypesCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/tests/AssertionTypesCheckTest.java index 5a91782436e..08f3562d7f3 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/tests/AssertionTypesCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/tests/AssertionTypesCheckTest.java @@ -20,6 +20,7 @@ import org.sonar.java.checks.helpers.JParserTestUtils; import org.sonar.java.checks.tests.AssertionTypesCheck.Argument; import org.sonar.java.checks.verifier.CheckVerifier; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.ExpressionStatementTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; @@ -31,6 +32,7 @@ import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; import static org.sonar.java.checks.verifier.TestUtils.testCodeSourcesPath; +@TestResourceScope.Scoped class AssertionTypesCheckTest { @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/unused/utils/AnnotationFieldReferenceFinderTest.java b/java-checks/src/test/java/org/sonar/java/checks/unused/utils/AnnotationFieldReferenceFinderTest.java index 0f89fe0f831..8e0f20a9102 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/unused/utils/AnnotationFieldReferenceFinderTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/unused/utils/AnnotationFieldReferenceFinderTest.java @@ -24,6 +24,7 @@ import org.sonar.java.checks.helpers.JParserTestUtils; import org.sonar.java.model.JavaTree; import org.sonar.java.model.declaration.VariableTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.VariableTree; @@ -33,6 +34,7 @@ * Tests an edge case of {@link AnnotationFieldReferenceFinder} that can not be covered with regular Java files by the tests in * {@link org.sonar.java.checks.unused.UnusedPrivateFieldCheckTest}. */ +@TestResourceScope.Scoped class AnnotationFieldReferenceFinderTest { @RegisterExtension private final LogTesterJUnit5 logTester = new LogTesterJUnit5().setLevel(Level.ERROR); diff --git a/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java b/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java index 09c44fc0ac2..8c44cd635fd 100644 --- a/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java +++ b/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -40,6 +39,7 @@ import org.sonar.java.collections.CollectionUtils; import org.sonar.java.exceptions.ApiMismatchException; import org.sonar.java.filters.SonarJavaIssueFilter; +import org.sonar.java.model.FrontendResources; import org.sonar.java.model.JParserConfig; import org.sonar.java.model.VisitorsBridge; import org.sonar.java.telemetry.Telemetry; @@ -236,23 +236,22 @@ private void scanInBatches(BatchModeContext context, List allInputFil private void scanBatch(BatchModeContext context, List batchFiles, AnalysisProgress analysisProgress) { analysisProgress.startBatch(batchFiles.size()); - Set environmentsCleaners = new HashSet<>(); - JParserConfig.Mode.BATCH - .create(javaVersion, context.getClasspath(), sonarComponents.shouldIgnoreUnnamedModuleForSplitPackage()) - .parse(batchFiles, sonarComponents::analysisCancelled, analysisProgress, (input, result) -> scanAsBatchCallback(input, result, context, environmentsCleaners)); - // Due to a bug in ECJ, JAR files remain locked after the analysis on Windows, we unlock them manually, at the end of each batches. See SONARJAVA-3609. - environmentsCleaners.forEach(Runnable::run); + try (FrontendResources environmentsCleaners = new FrontendResources()) { + JParserConfig.Mode.BATCH + .create(javaVersion, context.getClasspath(), sonarComponents.shouldIgnoreUnnamedModuleForSplitPackage()) + .parse(batchFiles, sonarComponents::analysisCancelled, analysisProgress, (input, result) -> scanAsBatchCallback(input, result, context, environmentsCleaners)); + } analysisProgress.endBatch(); } - private static void scanAsBatchCallback(InputFile inputFile, JParserConfig.Result result, BatchModeContext context, Set environmentsCleaners) { + private static void scanAsBatchCallback(InputFile inputFile, JParserConfig.Result result, BatchModeContext context, FrontendResources environmentsCleaners) { JavaAstScanner scanner = context.selectScanner(inputFile); Duration duration = PerformanceMeasure.start(context.descriptor(inputFile)); - scanner.simpleScan(inputFile, result, ast -> - // In batch mode, we delay the cleaning of the environment as it will be used in later processing. - environmentsCleaners.add(ast.sema.getEnvironmentCleaner()) - ); - duration.stop(); + try { + scanner.simpleScan(inputFile, result, ast -> environmentsCleaners.own(ast.sema.getEnvironmentCleaner())); + } finally { + duration.stop(); + } } interface BatchModeContext { diff --git a/java-frontend/src/main/java/org/sonar/java/ProgressMonitor.java b/java-frontend/src/main/java/org/sonar/java/ProgressMonitor.java index 74e3e81452c..dd1c71e52a2 100644 --- a/java-frontend/src/main/java/org/sonar/java/ProgressMonitor.java +++ b/java-frontend/src/main/java/org/sonar/java/ProgressMonitor.java @@ -19,12 +19,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; -import org.eclipse.core.runtime.IProgressMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.java.annotations.VisibleForTesting; -public class ProgressMonitor implements IProgressMonitor, Runnable { +public class ProgressMonitor implements Runnable { private final Logger logger; private final long period; @@ -84,7 +83,6 @@ public void run() { } } - @Override public void beginTask(String name, int totalWork) { if (totalWork <= 0) { unknownTotalWork = true; @@ -96,7 +94,6 @@ public void beginTask(String name, int totalWork) { thread.start(); } - @Override public void done() { if (success && analysisProgress.isLastBatch()) { log("100% analyzed"); @@ -107,7 +104,6 @@ public void done() { join(); } - @Override public boolean isCanceled() { if (isCanceled.getAsBoolean()) { log("Batch processing: Cancelled!"); @@ -116,7 +112,6 @@ public boolean isCanceled() { return false; } - @Override public void setCanceled(boolean value) { // do nothing } @@ -129,7 +124,6 @@ private void join() { } } - @Override public void worked(int work) { processedWork += work; if (processedWork == totalWork) { @@ -137,17 +131,14 @@ public void worked(int work) { } } - @Override public void internalWorked(double work) { // do nothing } - @Override public void setTaskName(String name) { // do nothing } - @Override public void subTask(String name) { // do nothing } diff --git a/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java b/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java index cbfb0b41964..f7bf25fb650 100644 --- a/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java +++ b/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java @@ -37,6 +37,7 @@ import org.sonar.java.AnalysisProgress; import org.sonar.java.SonarComponents; import org.sonar.java.annotations.VisibleForTesting; +import org.sonar.java.model.FrontendResources; import org.sonar.java.model.InputFileUtils; import org.sonar.java.model.JParserConfig; import org.sonar.java.model.JProblem; @@ -163,12 +164,14 @@ public void simpleScan(InputFile inputFile, JParserConfig.Result result, Consume var telemetryAnalysisKeys = javaAnalysisKeys.exceptions(); try { JavaTree.CompilationUnitTreeImpl ast = result.get(); - modifyCompilationUnit.accept(ast); - visitor.visitFile(ast, sonarComponents != null && sonarComponents.fileCanBeSkipped(inputFile)); - String path = inputFile.toString(); - Set undefinedTypes = ast.undefinedTypes(); - collectUndefinedTypes(path, undefinedTypes); - cleanUp.accept(ast); + Set undefinedTypes; + try (FrontendResources resources = new FrontendResources()) { + resources.own(() -> cleanUp.accept(ast)); + modifyCompilationUnit.accept(ast); + visitor.visitFile(ast, sonarComponents != null && sonarComponents.fileCanBeSkipped(inputFile)); + undefinedTypes = ast.undefinedTypes(); + collectUndefinedTypes(inputFile.toString(), undefinedTypes); + } telemetryAnalysisKeys = javaAnalysisKeys.success(); telemetry.aggregateAsCounter(javaAnalysisKeys.success().typeErrorCountKey(), undefinedTypes.size()); } catch (RecognitionException e) { diff --git a/java-frontend/src/main/java/org/sonar/java/model/EcjFrontendBackend.java b/java-frontend/src/main/java/org/sonar/java/model/EcjFrontendBackend.java new file mode 100644 index 00000000000..9b36a94bd77 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/EcjFrontendBackend.java @@ -0,0 +1,96 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.ASTUtils; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.FileASTRequestor; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.java.ProgressMonitor; + +final class EcjFrontendBackend implements FrontendBackend { + + private final Supplier parserFactory; + + EcjFrontendBackend(Supplier parserFactory) { + this.parserFactory = parserFactory; + } + + @Override + public FrontendSession open(FrontendConfiguration configuration) { + return new Session(configuration, parserFactory.get()); + } + + static ASTParser newParser(FrontendConfiguration configuration) { + ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); + Map options = new HashMap<>(JavaCore.getOptions()); + JavaCore.setComplianceOptions(configuration.sourceVersion(), options); + options.put(JavaCore.COMPILER_PB_MAX_PER_UNIT, "42000"); + if (configuration.ignoreUnnamedModuleForSplitPackage()) { + options.put(JavaCore.COMPILER_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE, "enabled"); + } + if (configuration.previewEnabled()) { + options.put(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, "enabled"); + } + EcjDiagnosticProvider.compilerOptions().forEach(option -> options.put(option, "warning")); + parser.setCompilerOptions(options); + boolean includeRunningVMBootclasspath = configuration.classpath().stream() + .noneMatch(file -> Set.of("rt.jar", "jrt-fs.jar", "android.jar").contains(file.getName())); + parser.setEnvironment(configuration.classpath().stream().map(File::getAbsolutePath).toArray(String[]::new), + new String[] {}, new String[] {}, includeRunningVMBootclasspath); + parser.setResolveBindings(true); + parser.setBindingsRecovery(true); + return parser; + } + + private static final class Session extends FrontendSession { + + private final FrontendConfiguration configuration; + private final ASTParser parser; + + private Session(FrontendConfiguration configuration, ASTParser parser) { + this.configuration = configuration; + this.parser = parser; + } + + @Override + JavaTree.CompilationUnitTreeImpl parse(String unitName, String source) { + return JParser.parse(parser, configuration.sourceVersion(), unitName, source, ast -> own(ASTUtils.getEnvironmentCleaner(ast.getAST()))); + } + + @Override + void parseBatch(FrontendBatch inputs, ProgressMonitor monitor, BiConsumer action) { + parser.createASTs(inputs.paths().toArray(String[]::new), inputs.encodings().toArray(String[]::new), new String[0], new FileASTRequestor() { + @Override + public void acceptAST(String sourceFilePath, CompilationUnit ast) { + Runnable cleaner = own(ASTUtils.getEnvironmentCleaner(ast.getAST())); + InputFile input = inputs.byPath().get(new File(sourceFilePath)); + action.accept(input, () -> JParser.convert(configuration.sourceVersion(), input.filename(), input.contents(), ast, cleaner)); + } + }, new EcjProgressMonitor(monitor)); + } + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/EcjProgressMonitor.java b/java-frontend/src/main/java/org/sonar/java/model/EcjProgressMonitor.java new file mode 100644 index 00000000000..2622e6bcb0b --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/EcjProgressMonitor.java @@ -0,0 +1,69 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.sonar.java.ProgressMonitor; + +final class EcjProgressMonitor implements IProgressMonitor { + + private final ProgressMonitor delegate; + + EcjProgressMonitor(ProgressMonitor delegate) { + this.delegate = delegate; + } + + @Override + public void beginTask(String name, int totalWork) { + delegate.beginTask(name, totalWork); + } + + @Override + public void done() { + delegate.done(); + } + + @Override + public void worked(int work) { + delegate.worked(work); + } + + @Override + public boolean isCanceled() { + return delegate.isCanceled(); + } + + @Override + public void setCanceled(boolean value) { + delegate.setCanceled(value); + } + + @Override + public void internalWorked(double work) { + delegate.internalWorked(work); + } + + @Override + public void setTaskName(String name) { + delegate.setTaskName(name); + } + + @Override + public void subTask(String name) { + delegate.subTask(name); + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/FrontendBackend.java b/java-frontend/src/main/java/org/sonar/java/model/FrontendBackend.java new file mode 100644 index 00000000000..030a6ed9ce7 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/FrontendBackend.java @@ -0,0 +1,23 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +interface FrontendBackend { + + FrontendSession open(FrontendConfiguration configuration); + +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/FrontendBatch.java b/java-frontend/src/main/java/org/sonar/java/model/FrontendBatch.java new file mode 100644 index 00000000000..09f7e7ccb1e --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/FrontendBatch.java @@ -0,0 +1,49 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.sonar.api.batch.fs.InputFile; + +record FrontendBatch(List inputs, List paths, List encodings, Map byPath) { + + FrontendBatch { + inputs = List.copyOf(inputs); + paths = List.copyOf(paths); + encodings = List.copyOf(encodings); + byPath = Map.copyOf(byPath); + } + + static FrontendBatch from(Iterable inputFiles) { + List inputs = new ArrayList<>(); + List paths = new ArrayList<>(); + List encodings = new ArrayList<>(); + Map byPath = new HashMap<>(); + for (InputFile input : inputFiles) { + String path = input.absolutePath(); + byPath.put(new File(path), input); + paths.add(path); + encodings.add(input.charset().name()); + inputs.add(input); + } + return new FrontendBatch(inputs, paths, encodings, byPath); + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/FrontendConfiguration.java b/java-frontend/src/main/java/org/sonar/java/model/FrontendConfiguration.java new file mode 100644 index 00000000000..7c9fe9497d8 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/FrontendConfiguration.java @@ -0,0 +1,32 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.io.File; +import java.util.List; +import org.sonar.plugins.java.api.JavaVersion; + +record FrontendConfiguration(String sourceVersion, boolean previewEnabled, List classpath, boolean ignoreUnnamedModuleForSplitPackage) { + + FrontendConfiguration { + classpath = List.copyOf(classpath); + } + + static FrontendConfiguration from(JavaVersion version, List classpath, boolean ignoreUnnamedModuleForSplitPackage) { + return new FrontendConfiguration(version.effectiveJavaVersionAsString(), version.arePreviewFeaturesEnabled(), classpath, ignoreUnnamedModuleForSplitPackage); + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/FrontendParser.java b/java-frontend/src/main/java/org/sonar/java/model/FrontendParser.java new file mode 100644 index 00000000000..4d03b40e8cd --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/FrontendParser.java @@ -0,0 +1,68 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.io.File; +import java.util.List; +import org.sonar.plugins.java.api.JavaVersion; + +public final class FrontendParser { + + private FrontendParser() { + } + + public static ParsedFile parse(JavaVersion version, List classpath, String unitName, String source) { + JParserConfig config = JParserConfig.Mode.FILE_BY_FILE.create(version, classpath); + return parse(config.backend(), config.configuration, unitName, source); + } + + /** Closes the session after parsing exceptions or errors, preserving the parsing failure and suppressing cleanup failures. */ + static ParsedFile parse(FrontendBackend backend, FrontendConfiguration configuration, String unitName, String source) { + FrontendSession session = backend.open(configuration); + try { + return new ParsedFile(session.parse(unitName, source), session); + } catch (RuntimeException | Error failure) { + try { + session.close(); + } catch (RuntimeException | Error cleanupFailure) { + if (failure != cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + } + throw failure; + } + } + + public static final class ParsedFile implements AutoCloseable { + private final JavaTree.CompilationUnitTreeImpl tree; + private final FrontendSession session; + + private ParsedFile(JavaTree.CompilationUnitTreeImpl tree, FrontendSession session) { + this.tree = tree; + this.session = session; + } + + public JavaTree.CompilationUnitTreeImpl tree() { + return tree; + } + + @Override + public void close() { + session.close(); + } + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/FrontendResources.java b/java-frontend/src/main/java/org/sonar/java/model/FrontendResources.java new file mode 100644 index 00000000000..129dccd1145 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/FrontendResources.java @@ -0,0 +1,96 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** Owns cleanup handles until the last supported semantic consumer finishes. */ +public final class FrontendResources implements AutoCloseable { + + private final Map cleaners = new LinkedHashMap<>(); + private boolean closed; + + public Runnable own(Runnable cleaner) { + if (closed) { + throw new IllegalStateException("Frontend resources are closed"); + } + return cleaners.computeIfAbsent(cleaner, FrontendResources::idempotent); + } + + static Runnable idempotent(Runnable cleaner) { + return new Cleaner(cleaner); + } + + /** Attempts every cleaner even after errors, then rethrows the first failure with later failures suppressed. */ + @Override + public void close() { + if (closed) { + return; + } + closed = true; + Throwable failure = null; + try { + for (Runnable cleaner : cleaners.values()) { + try { + cleaner.run(); + } catch (RuntimeException | Error e) { + if (failure == null) { + failure = e; + } else if (failure != e) { + failure.addSuppressed(e); + } + } + } + } finally { + cleaners.clear(); + } + if (failure instanceof Error error) { + throw error; + } + if (failure instanceof RuntimeException exception) { + throw exception; + } + } + + private static final class Cleaner implements Runnable { + private final Runnable delegate; + private boolean cleaned; + + private Cleaner(Runnable delegate) { + this.delegate = delegate; + } + + @Override + public synchronized void run() { + if (!cleaned) { + cleaned = true; + delegate.run(); + } + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof Cleaner cleaner && delegate.equals(cleaner.delegate)); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/FrontendSession.java b/java-frontend/src/main/java/org/sonar/java/model/FrontendSession.java new file mode 100644 index 00000000000..b75067bf671 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/FrontendSession.java @@ -0,0 +1,45 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.io.IOException; +import java.util.function.BiConsumer; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.java.ProgressMonitor; + +abstract class FrontendSession implements AutoCloseable { + + private final FrontendResources resources = new FrontendResources(); + + final Runnable own(Runnable cleaner) { + return resources.own(cleaner); + } + + abstract JavaTree.CompilationUnitTreeImpl parse(String unitName, String source); + + abstract void parseBatch(FrontendBatch inputs, ProgressMonitor monitor, BiConsumer action); + + @Override + public void close() { + resources.close(); + } + + @FunctionalInterface + interface ParsedUnit { + JavaTree.CompilationUnitTreeImpl convert() throws IOException; + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParser.java b/java-frontend/src/main/java/org/sonar/java/model/JParser.java index 6ddf24dee0d..4adbc9e5539 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParser.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParser.java @@ -28,6 +28,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -302,6 +303,10 @@ private KeywordSuper keywordSuper(InternalSyntaxToken token, @Nullable ITypeBind * @throws RecognitionException in case of syntax errors */ public static JavaTree.CompilationUnitTreeImpl parse(ASTParser astParser, String version, String unitName, String source) { + return parse(astParser, version, unitName, source, ast -> null); + } + + static JavaTree.CompilationUnitTreeImpl parse(ASTParser astParser, String version, String unitName, String source, Function environmentOwner) { astParser.setUnitName(unitName); astParser.setSource(source.toCharArray()); @@ -313,10 +318,15 @@ public static JavaTree.CompilationUnitTreeImpl parse(ASTParser astParser, String throw new RecognitionException(-1, "ECJ: Unable to parse file.", e); } - return convert(version, unitName, source, astNode); + return convert(version, unitName, source, astNode, environmentOwner.apply(astNode)); } static JavaTree.CompilationUnitTreeImpl convert(String version, String unitName, String source, CompilationUnit astNode) { + return convert(version, unitName, source, astNode, null); + } + + static JavaTree.CompilationUnitTreeImpl convert(String version, String unitName, String source, CompilationUnit astNode, + @Nullable Runnable environmentCleaner) { List errors = Stream.of(astNode.getProblems()).filter(IProblem::isError).toList(); Optional possibleSyntaxError = errors.stream().filter(IS_SYNTAX_ERROR).findFirst(); LineColumnConverter lineColumnConverter = new LineColumnConverter(source); @@ -336,7 +346,7 @@ static JavaTree.CompilationUnitTreeImpl convert(String version, String unitName, .collect(Collectors.toSet()); JParser converter = new JParser(); - converter.sema = new JSema(astNode.getAST()); + converter.sema = new JSema(astNode.getAST(), environmentCleaner); converter.compilationUnit = astNode; converter.tokenManager = createTokenManager(version, unitName, source); converter.lineColumnConverter = lineColumnConverter; diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java b/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java index 5ef2bb5400c..8debd5fa9a2 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java @@ -17,24 +17,15 @@ package org.sonar.java.model; import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import java.util.stream.StreamSupport; import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.FileASTRequestor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputFile; @@ -53,17 +44,10 @@ public abstract class JParserConfig { private static final Logger LOG = LoggerFactory.getLogger(JParserConfig.class); - private static final String MAXIMUM_ECJ_WARNINGS = "42000"; - private static final Set JRE_JARS = new HashSet<>(Arrays.asList("rt.jar", "jrt-fs.jar", "android.jar")); - - final JavaVersion javaVersion; - final List classpath; - final boolean shouldIgnoreUnnamedModuleForSplitPackage; + final FrontendConfiguration configuration; private JParserConfig(JavaVersion javaVersion, List classpath, boolean shouldIgnoreUnnamedModuleForSplitPackage) { - this.javaVersion = javaVersion; - this.classpath = classpath; - this.shouldIgnoreUnnamedModuleForSplitPackage = shouldIgnoreUnnamedModuleForSplitPackage; + this.configuration = FrontendConfiguration.from(javaVersion, classpath, shouldIgnoreUnnamedModuleForSplitPackage); } public abstract void parse(Iterable inputFiles, BooleanSupplier isCanceled, @@ -114,33 +98,15 @@ public JavaTree.CompilationUnitTreeImpl get() throws Exception { } public ASTParser astParser() { - ASTParser astParser = ASTParser.newParser(AST.getJLSLatest()); - Map options = new HashMap<>(JavaCore.getOptions()); - JavaCore.setComplianceOptions(javaVersion.effectiveJavaVersionAsString(), options); - options.put(JavaCore.COMPILER_PB_MAX_PER_UNIT, MAXIMUM_ECJ_WARNINGS); - if (shouldIgnoreUnnamedModuleForSplitPackage) { - options.put(JavaCore.COMPILER_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE, "enabled"); - } - if (shouldEnablePreviewFlag(javaVersion)) { - options.put(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, "enabled"); - } - // enabling all supported compiler warnings - EcjDiagnosticProvider.compilerOptions() - .forEach(option -> options.put(option, "warning")); - - astParser.setCompilerOptions(options); - - boolean includeRunningVMBootclasspath = classpath.stream() - .noneMatch(f -> JRE_JARS.contains(f.getName())); - - astParser.setEnvironment(classpath.stream() - .map(File::getAbsolutePath) - .toArray(String[]::new), new String[] {}, new String[] {}, includeRunningVMBootclasspath); + return EcjFrontendBackend.newParser(configuration); + } - astParser.setResolveBindings(true); - astParser.setBindingsRecovery(true); + FrontendBackend backend() { + return new EcjFrontendBackend(this::astParser); + } - return astParser; + final FrontendSession openSession() { + return backend().open(configuration); } @VisibleForTesting @@ -154,45 +120,32 @@ static class Batch extends JParserConfig { public void parse(Iterable inputFiles, BooleanSupplier isCanceled, AnalysisProgress analysisProgress, BiConsumer action) { - List sourceFilePaths = new ArrayList<>(); - Set notYetAnalyzedFiles = new LinkedHashSet<>(); - List encodings = new ArrayList<>(); - Map inputs = new HashMap<>(); - for (InputFile inputFile : inputFiles) { - String sourceFilePath = inputFile.absolutePath(); - inputs.put(new File(sourceFilePath), inputFile); - sourceFilePaths.add(sourceFilePath); - encodings.add(inputFile.charset().name()); - notYetAnalyzedFiles.add(inputFile); - } + FrontendBatch inputs = FrontendBatch.from(inputFiles); + Set notYetAnalyzedFiles = new LinkedHashSet<>(inputs.inputs()); ExecutionTimeReport executionTimeReport = new ExecutionTimeReport(); ProgressMonitor monitor = new ProgressMonitor(isCanceled, analysisProgress); PerformanceMeasure.Duration batchPerformance = PerformanceMeasure.start("ParseAsBatch"); - try { - astParser().createASTs(sourceFilePaths.toArray(new String[0]), encodings.toArray(new String[0]), new String[0], new FileASTRequestor() { - @Override - public void acceptAST(String sourceFilePath, CompilationUnit ast) { - PerformanceMeasure.Duration convertDuration = PerformanceMeasure.start("Convert"); - InputFile inputFile = inputs.get(new File(sourceFilePath)); - executionTimeReport.start(inputFile); - Result result; - try { - result = new Result(JParser.convert(javaVersion.effectiveJavaVersionAsString(), inputFile.filename(), inputFile.contents(), ast)); - } catch (Exception e) { - result = new Result(e); - } - convertDuration.stop(); - PerformanceMeasure.Duration analyzeDuration = PerformanceMeasure.start("Analyze"); - action.accept(inputFile, result); - - notYetAnalyzedFiles.remove(inputFile); - executionTimeReport.end(); - analyzeDuration.stop(); + try (FrontendSession session = openSession()) { + session.parseBatch(inputs, monitor, (inputFile, unit) -> { + PerformanceMeasure.Duration convertDuration = PerformanceMeasure.start("Convert"); + executionTimeReport.start(inputFile); + Result result; + try { + result = new Result(unit.convert()); + } catch (Exception e) { + result = new Result(e); } - }, monitor); + convertDuration.stop(); + PerformanceMeasure.Duration analyzeDuration = PerformanceMeasure.start("Analyze"); + action.accept(inputFile, result); + + notYetAnalyzedFiles.remove(inputFile); + executionTimeReport.end(); + analyzeDuration.stop(); + }); if (!notYetAnalyzedFiles.isEmpty()) { - String message = String.format("%d/%d files were not analyzed by the batch mode", notYetAnalyzedFiles.size(), sourceFilePaths.size()); + String message = String.format("%d/%d files were not analyzed by the batch mode", notYetAnalyzedFiles.size(), inputs.inputs().size()); throw new AnalysisException(message); } } catch (OperationCanceledException e) { @@ -216,7 +169,9 @@ private void fallbackToFileByFileMode(List inputFiles, BooleanSupplie if (isCanceled.getAsBoolean()) { break; } - FileByFile.parse(astParser(), inputFile, javaVersion, action); + try (FrontendSession session = openSession()) { + FileByFile.parse(session, inputFile, action); + } } } @@ -247,7 +202,9 @@ public void parse(Iterable inputFiles, BooleanSupplier isCa break; } executionTimeReport.start(inputFile); - parse(astParser(), inputFile, javaVersion, action); + try (FrontendSession session = openSession()) { + parse(session, inputFile, action); + } executionTimeReport.end(); progressReport.nextFile(); } @@ -262,11 +219,11 @@ public void parse(Iterable inputFiles, BooleanSupplier isCa } } - private static void parse(ASTParser astParser, InputFile inputFile, JavaVersion javaVersion, BiConsumer action) { + private static void parse(FrontendSession session, InputFile inputFile, BiConsumer action) { Result result; PerformanceMeasure.Duration parseDuration = PerformanceMeasure.start("JParser"); try { - result = new Result(JParser.parse(astParser, javaVersion.effectiveJavaVersionAsString(), inputFile.filename(), inputFile.contents())); + result = new Result(session.parse(inputFile.filename(), inputFile.contents())); } catch (Exception e) { result = new Result(e); } finally { diff --git a/java-frontend/src/main/java/org/sonar/java/model/JSema.java b/java-frontend/src/main/java/org/sonar/java/model/JSema.java index 201cee203f9..0f0cfa11c5d 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JSema.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JSema.java @@ -40,6 +40,8 @@ public final class JSema implements Sema { private final AST ast; + @Nullable + private Runnable environmentCleaner; final Map declarations = new HashMap<>(); final Map> usages = new HashMap<>(); private final Map types = new HashMap<>(); @@ -50,7 +52,12 @@ public final class JSema implements Sema { private final Map nameToTypeCache = new HashMap<>(); JSema(AST ast) { + this(ast, null); + } + + JSema(AST ast, @Nullable Runnable environmentCleaner) { this.ast = ast; + this.environmentCleaner = environmentCleaner; } Supplier typeSupplier(@Nullable ITypeBinding binding) { @@ -205,7 +212,10 @@ IAnnotationBinding[] resolvePackageAnnotations(String packageName) { @Override public Runnable getEnvironmentCleaner() { - return ASTUtils.getEnvironmentCleaner(ast); + if (environmentCleaner == null) { + environmentCleaner = FrontendResources.idempotent(ASTUtils.getEnvironmentCleaner(ast)); + } + return environmentCleaner; } } diff --git a/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java b/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java index dbd2713a525..8bc1a02f233 100644 --- a/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java @@ -83,6 +83,7 @@ import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.JavaTree; import org.sonar.java.reporting.AnalyzerMessage; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.testing.ThreadLocalLogTester; import org.sonar.plugins.java.api.CheckRegistrar; import org.sonar.plugins.java.api.JavaCheck; @@ -117,6 +118,7 @@ import static org.sonar.java.TestUtils.computeLineEndOffsets; @ExtendWith(MockitoExtension.class) +@TestResourceScope.Scoped class SonarComponentsTest { private static final Version V8_9 = Version.create(8, 9); diff --git a/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java b/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java index 33339029e15..935e20404f7 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java @@ -57,6 +57,7 @@ import org.sonar.java.notchecks.VisitorNotInChecksPackage; import org.sonar.java.telemetry.NoOpTelemetry; import org.sonar.java.telemetry.TelemetryKey; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.testing.ThreadLocalLogTester; import org.sonar.plugins.java.api.JavaFileScanner; import org.sonar.plugins.java.api.JavaFileScannerContext; @@ -82,6 +83,7 @@ import static org.mockito.Mockito.when; import static org.sonar.java.model.EcjTestSupport.clearSymbol; +@TestResourceScope.Scoped class JavaAstScannerTest { public static final InputFile TRIVIAL_COMPILATION_UNIT = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt"); diff --git a/java-frontend/src/test/java/org/sonar/java/ast/visitors/AccessorsUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/ast/visitors/AccessorsUtilsTest.java index b38e862d82c..3f972bec6bd 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/visitors/AccessorsUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/visitors/AccessorsUtilsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.MethodTree; @@ -25,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class AccessorsUtilsTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/ast/visitors/ComplexityVisitorTest.java b/java-frontend/src/test/java/org/sonar/java/ast/visitors/ComplexityVisitorTest.java index 4fceb50994d..e838fdcdb56 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/visitors/ComplexityVisitorTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/visitors/ComplexityVisitorTest.java @@ -19,6 +19,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ExpressionTree; @@ -28,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class ComplexityVisitorTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/ast/visitors/MethodNestingLevelVisitorTest.java b/java-frontend/src/test/java/org/sonar/java/ast/visitors/MethodNestingLevelVisitorTest.java index ab4b5109d0e..02220495fac 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/visitors/MethodNestingLevelVisitorTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/visitors/MethodNestingLevelVisitorTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.MethodTree; @@ -26,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class MethodNestingLevelVisitorTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/ast/visitors/NumberOfDefinedVariablesVisitorTest.java b/java-frontend/src/test/java/org/sonar/java/ast/visitors/NumberOfDefinedVariablesVisitorTest.java index 04c6022f69e..14d1d7c2cfb 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/visitors/NumberOfDefinedVariablesVisitorTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/visitors/NumberOfDefinedVariablesVisitorTest.java @@ -18,12 +18,14 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.MethodTree; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class NumberOfDefinedVariablesVisitorTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/ast/visitors/PublicApiCheckerTest.java b/java-frontend/src/test/java/org/sonar/java/ast/visitors/PublicApiCheckerTest.java index 6ecd8fa3cc4..d27c956bf19 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/visitors/PublicApiCheckerTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/visitors/PublicApiCheckerTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.IdentifierTree; @@ -40,6 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class PublicApiCheckerTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/cfg/CFGLoopTest.java b/java-frontend/src/test/java/org/sonar/java/cfg/CFGLoopTest.java index e03e71eadec..870de0470d4 100644 --- a/java-frontend/src/test/java/org/sonar/java/cfg/CFGLoopTest.java +++ b/java-frontend/src/test/java/org/sonar/java/cfg/CFGLoopTest.java @@ -19,6 +19,7 @@ import java.util.Comparator; import org.junit.jupiter.api.Test; import org.sonar.java.cfg.CFG.Block; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.Tree; import java.util.ArrayList; @@ -28,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class CFGLoopTest { private static final CFGTestLoader loader = new CFGTestLoader("src/test/files/cfg/CFGLoopTest.java"); diff --git a/java-frontend/src/test/java/org/sonar/java/cfg/CFGTest.java b/java-frontend/src/test/java/org/sonar/java/cfg/CFGTest.java index 543bd51f429..6998fc0230f 100644 --- a/java-frontend/src/test/java/org/sonar/java/cfg/CFGTest.java +++ b/java-frontend/src/test/java/org/sonar/java/cfg/CFGTest.java @@ -29,6 +29,7 @@ import org.sonar.java.cfg.CFG.Block; import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.LiteralUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.testing.ThreadLocalLogTester; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -101,6 +102,7 @@ import static org.sonar.plugins.java.api.tree.Tree.Kind.WHILE_STATEMENT; import static org.sonar.plugins.java.api.tree.Tree.Kind.YIELD_STATEMENT; +@TestResourceScope.Scoped class CFGTest { @RegisterExtension diff --git a/java-frontend/src/test/java/org/sonar/java/cfg/CFGUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/cfg/CFGUtilsTest.java index 35e33618dbe..ada56fcb46c 100644 --- a/java-frontend/src/test/java/org/sonar/java/cfg/CFGUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/cfg/CFGUtilsTest.java @@ -18,12 +18,14 @@ import org.junit.jupiter.api.Test; import org.sonar.java.cfg.CFG.Block; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.Tree; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped class CFGUtilsTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/cfg/LiveVariablesTest.java b/java-frontend/src/test/java/org/sonar/java/cfg/LiveVariablesTest.java index 47885c13347..59fc334f9f7 100644 --- a/java-frontend/src/test/java/org/sonar/java/cfg/LiveVariablesTest.java +++ b/java-frontend/src/test/java/org/sonar/java/cfg/LiveVariablesTest.java @@ -20,6 +20,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -27,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class LiveVariablesTest { private static CFG buildCFG(String methodCode) { diff --git a/java-frontend/src/test/java/org/sonar/java/cfg/VariableReadExtractorTest.java b/java-frontend/src/test/java/org/sonar/java/cfg/VariableReadExtractorTest.java index 832111d303f..fb53fc6ee6c 100644 --- a/java-frontend/src/test/java/org/sonar/java/cfg/VariableReadExtractorTest.java +++ b/java-frontend/src/test/java/org/sonar/java/cfg/VariableReadExtractorTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.MethodTree; @@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class VariableReadExtractorTest { private static MethodTree buildMethodTree(String methodCode) { diff --git a/java-frontend/src/test/java/org/sonar/java/matcher/TreeMatcherTest.java b/java-frontend/src/test/java/org/sonar/java/matcher/TreeMatcherTest.java index 2b87ca16ebb..cc9a724069a 100644 --- a/java-frontend/src/test/java/org/sonar/java/matcher/TreeMatcherTest.java +++ b/java-frontend/src/test/java/org/sonar/java/matcher/TreeMatcherTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.BlockTree; @@ -53,6 +54,7 @@ import static org.sonar.java.matcher.TreeMatcher.withBody; import static org.sonar.java.matcher.TreeMatcher.withExpression; +@TestResourceScope.Scoped class TreeMatcherTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/metrics/MetricsComputerTest.java b/java-frontend/src/test/java/org/sonar/java/metrics/MetricsComputerTest.java index b6970188986..3948cc37bd4 100644 --- a/java-frontend/src/test/java/org/sonar/java/metrics/MetricsComputerTest.java +++ b/java-frontend/src/test/java/org/sonar/java/metrics/MetricsComputerTest.java @@ -18,12 +18,14 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.MethodTree; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class MetricsComputerTest implements MetricsScannerContext { MetricsComputer mc = new MetricsComputer(); diff --git a/java-frontend/src/test/java/org/sonar/java/model/DefaultInputFileScannerContextTest.java b/java-frontend/src/test/java/org/sonar/java/model/DefaultInputFileScannerContextTest.java index e5d137c97fc..93ccb881910 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/DefaultInputFileScannerContextTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/DefaultInputFileScannerContextTest.java @@ -26,6 +26,7 @@ import org.sonar.java.SonarComponents; import org.sonar.java.TestUtils; import org.sonar.java.reporting.AnalyzerMessage; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.ModuleScannerContext; import org.sonar.plugins.java.api.internal.EndOfAnalysis; @@ -41,6 +42,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped class DefaultInputFileScannerContextTest { protected static final File JAVA_FILE = new File("src/test/files/api/JavaFileScannerContext.java"); diff --git a/java-frontend/src/test/java/org/sonar/java/model/DefaultJavaFileScannerContextTest.java b/java-frontend/src/test/java/org/sonar/java/model/DefaultJavaFileScannerContextTest.java index 9bc2430d3c1..83f54033aef 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/DefaultJavaFileScannerContextTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/DefaultJavaFileScannerContextTest.java @@ -26,6 +26,7 @@ import org.sonar.java.reporting.AnalyzerMessage; import org.sonar.java.reporting.AnalyzerMessage.TextSpan; import org.sonar.java.reporting.FluentReporting; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.JavaFileScannerContext.Location; import org.sonar.plugins.java.api.SourceMap; @@ -44,6 +45,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped class DefaultJavaFileScannerContextTest extends DefaultInputFileScannerContextTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/DefaultJavaFileScannerContextWithSensorContextTest.java b/java-frontend/src/test/java/org/sonar/java/model/DefaultJavaFileScannerContextWithSensorContextTest.java index 501bd6d2e56..635a69a867e 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/DefaultJavaFileScannerContextWithSensorContextTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/DefaultJavaFileScannerContextWithSensorContextTest.java @@ -40,6 +40,7 @@ import org.sonar.java.TestUtils; import org.sonar.java.classpath.ClasspathForMain; import org.sonar.java.classpath.ClasspathForTest; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -56,6 +57,7 @@ * */ @ExtendWith(MockitoExtension.class) +@TestResourceScope.Scoped class DefaultJavaFileScannerContextWithSensorContextTest { @Mock private FileLinesContextFactory fileLinesContextFactory; diff --git a/java-frontend/src/test/java/org/sonar/java/model/ExpressionUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/model/ExpressionUtilsTest.java index e00b3e2eb9c..8ae8aa7eb26 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/ExpressionUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/ExpressionUtilsTest.java @@ -27,6 +27,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.sonar.java.model.expression.ParenthesizedTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; @@ -57,6 +58,7 @@ import static org.sonar.java.model.ExpressionUtils.skipParenthesesUpwards; import static org.sonar.java.model.assertions.TreeAssert.assertThat; +@TestResourceScope.Scoped class ExpressionUtilsTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/FrontendContractSnapshot.java b/java-frontend/src/test/java/org/sonar/java/model/FrontendContractSnapshot.java new file mode 100644 index 00000000000..765c81c7c49 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/FrontendContractSnapshot.java @@ -0,0 +1,196 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.java.reporting.AnalyzerMessage; +import org.sonar.java.reporting.JavaQuickFix; +import org.sonar.java.testing.JavaFileScannerContextForTests; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.LambdaExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.SyntaxToken; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.TypeParameterTree; +import org.sonar.plugins.java.api.tree.VariableTree; + +public final class FrontendContractSnapshot { + + public static final Gson JSON = new GsonBuilder().serializeNulls().disableHtmlEscaping().create(); + + private final List trees = new ArrayList<>(); + private final Map treeIds = new IdentityHashMap<>(); + private final Map symbolIds = new IdentityHashMap<>(); + private final Map typeIds = new IdentityHashMap<>(); + + public static String json(Object value) { + return JSON.toJson(canonical(value)); + } + + private static Object canonical(Object value) { + if (value instanceof Map map) { + Map sorted = new TreeMap<>(); + map.forEach((key, entry) -> sorted.put((String) key, canonical(entry))); + return sorted; + } + if (value instanceof List list) { + return list.stream().map(FrontendContractSnapshot::canonical).toList(); + } + return value; + } + + public Map capture(JavaTree.CompilationUnitTreeImpl root) { + index(root); + List> nodes = new ArrayList<>(); + for (Tree tree : trees) { + Map node = new LinkedHashMap<>(); + node.put("id", treeIds.get(tree)); + node.put("kind", tree.kind().name()); + node.put("class", tree.getClass().getName()); + node.put("parent", treeIds.get(tree.parent())); + node.put("firstToken", treeIds.get(tree.firstToken())); + node.put("lastToken", treeIds.get(tree.lastToken())); + node.put("children", children(tree).stream().map(treeIds::get).toList()); + if (tree instanceof SyntaxToken token) { + node.put("text", token.text()); + node.put("range", token.range().toString()); + node.put("trivia", token.trivias().stream().map(trivia -> List.of(trivia.commentKind().name(), trivia.comment(), trivia.commentContent(), trivia.range().toString())).toList()); + } + if (tree instanceof ExpressionTree expression) { + Type type = expression.symbolType(); + node.put("type", type(type)); + node.put("typeIdentity", typeIds.computeIfAbsent(type, ignored -> typeIds.size())); + } + Symbol symbol = symbol(tree); + if (symbol != null) { + node.put("symbol", symbol(symbol)); + node.put("symbolIdentity", symbolIds.computeIfAbsent(symbol, ignored -> symbolIds.size())); + node.put("declaration", treeIds.get(symbol.declaration())); + node.put("usages", symbol.usages().stream().map(treeIds::get).toList()); + } + nodes.add(node); + } + List> warnings = new ArrayList<>(); + for (JProblem.Type kind : JProblem.Type.values()) { + root.warnings(kind).stream().sorted(Comparator.comparing(JWarning::start).thenComparing(JWarning::end).thenComparing(JWarning::message)) + .forEach(warning -> warnings.add(Map.of("type", kind.name(), "message", warning.message(), "start", warning.start().toString(), + "end", warning.end().toString(), "tree", treeIds.get(warning.syntaxTree())))); + } + return Map.of("nodes", nodes, "warnings", warnings, + "undefinedTypes", root.undefinedTypes().stream().map(problem -> List.of(problem.type().name(), problem.message(), problem.toString())).sorted(Comparator.comparing(Object::toString)).toList(), + "equalSymbols", equalPairs(symbolIds), "equalTypes", equalPairs(typeIds)); + } + + private void index(Tree tree) { + if (treeIds.containsKey(tree)) { + return; + } + treeIds.put(tree, trees.size()); + trees.add(tree); + children(tree).forEach(this::index); + } + + private static List children(Tree tree) { + return tree.is(Tree.Kind.TOKEN, Tree.Kind.INFERED_TYPE) ? List.of() : ((JavaTree) tree).getChildren(); + } + + @Nullable + private static Symbol symbol(Tree tree) { + return switch (tree) { + case ClassTree declaration -> declaration.symbol(); + case MethodTree declaration -> declaration.symbol(); + case VariableTree declaration -> declaration.symbol(); + case TypeParameterTree parameter -> parameter.symbol(); + case IdentifierTree identifier -> identifier.symbol(); + case MethodInvocationTree invocation -> invocation.methodSymbol(); + case LambdaExpressionTree lambda -> lambda.symbol(); + default -> null; + }; + } + + private static Map type(Type type) { + return Map.of("name", type.name(), "fullyQualifiedName", type.fullyQualifiedName(), "unknown", type.isUnknown(), "array", type.isArray()); + } + + private static Map symbol(Symbol symbol) { + Map result = new LinkedHashMap<>(); + result.put("name", symbol.name()); + result.put("unknown", symbol.isUnknown()); + result.put("type", type(symbol.type())); + result.put("owner", symbol.owner() == null ? null : symbol.owner().name()); + if (symbol instanceof Symbol.MethodSymbol method) { + result.put("signature", method.signature()); + result.put("parameters", method.parameterTypes().stream().map(FrontendContractSnapshot::type).toList()); + result.put("throws", method.thrownTypes().stream().map(FrontendContractSnapshot::type).toList()); + } + result.put("annotations", symbol.metadata().annotations().stream().map(annotation -> annotation.symbol().type().fullyQualifiedName()).toList()); + result.put("nullability", symbol.metadata().nullabilityData().type().name()); + return result; + } + + private static List> equalPairs(Map identities) { + List values = identities.keySet().stream().sorted(Comparator.comparingInt(identities::get)).toList(); + List> pairs = new ArrayList<>(); + for (int i = 0; i < values.size(); i++) { + for (int j = 0; j < values.size(); j++) { + if (values.get(i).equals(values.get(j))) { + pairs.add(List.of(i, j)); + } + } + } + return pairs; + } + + public static List> issues(JavaFileScannerContextForTests context) { + return context.getIssues().stream().map(issue -> { + Map result = message(issue); + result.put("check", issue.getCheck().getClass().getName()); + result.put("flows", issue.flows.stream().map(flow -> flow.stream().map(FrontendContractSnapshot::message).toList()).toList()); + result.put("quickFixes", context.getQuickFixes().getOrDefault(issue.primaryLocation(), List.of()).stream().map(FrontendContractSnapshot::quickFix).toList()); + return result; + }).toList(); + } + + private static Map message(AnalyzerMessage issue) { + Map result = new LinkedHashMap<>(); + result.put("message", issue.getMessage()); + result.put("cost", issue.getCost()); + result.put("location", issue.primaryLocation() == null ? null : issue.primaryLocation().toString()); + result.put("input", issue.getInputComponent() instanceof InputFile file ? file.relativePath() : issue.getInputComponent().key()); + return result; + } + + private static Map quickFix(JavaQuickFix fix) { + return Map.of("description", fix.getDescription(), "edits", fix.getTextEdits().stream() + .map(edit -> Map.of("range", edit.getTextSpan().toString(), "replacement", edit.getReplacement())).toList()); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/FrontendContractSnapshotTest.java b/java-frontend/src/test/java/org/sonar/java/model/FrontendContractSnapshotTest.java new file mode 100644 index 00000000000..e16649e566a --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/FrontendContractSnapshotTest.java @@ -0,0 +1,123 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.sonar.java.TestUtils; +import org.sonar.java.ast.JavaAstScanner; +import org.sonar.java.reporting.JavaQuickFix; +import org.sonar.java.reporting.JavaTextEdit; +import org.sonar.java.test.TestResourceScope; +import org.sonar.java.testing.VisitorsBridgeForTests; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.ReturnStatementTree; +import org.sonar.plugins.java.api.tree.Tree; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestResourceScope.Scoped +class FrontendContractSnapshotTest { + + @Test + void contract_records_are_repeatable_and_can_be_exported() throws Exception { + Map first = records(); + Map second = records(); + String json = FrontendContractSnapshot.json(first); + assertThat(json).isEqualTo(FrontendContractSnapshot.json(second)) + .contains("UNUSED_IMPORT", "UNKNOWN", "quickFixes", "replacement", "equalSymbols", "equalTypes"); + String output = System.getProperty("frontend.contract.output"); + if (output != null) { + Files.writeString(Path.of(output), json + "\n"); + } + } + + @Test + void generic_invocations_preserve_declaration_and_instantiation_identity() { + JavaTree.CompilationUnitTreeImpl tree = (JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse( + "class A { T id(T t) { return t; } String a() { return id(\"\"); } Integer b() { return id(1); } }"); + try { + ClassTree type = (ClassTree) tree.types().get(0); + MethodInvocationTree first = invocation((MethodTree) type.members().get(1)); + MethodInvocationTree second = invocation((MethodTree) type.members().get(2)); + assertThat(first.methodSymbol()).isSameAs(first.methodSymbol()).isNotEqualTo(second.methodSymbol()); + assertThat(first.methodSymbol().declaration()).isSameAs(second.methodSymbol().declaration()); + assertThat(first.methodSymbol().parameterTypes().get(0).fullyQualifiedName()).isEqualTo("java.lang.String"); + assertThat(second.methodSymbol().parameterTypes().get(0).fullyQualifiedName()).isEqualTo("java.lang.Integer"); + assertThat(first.symbolType()).isSameAs(first.symbolType()); + } finally { + tree.sema.getEnvironmentCleaner().run(); + } + } + + private static MethodInvocationTree invocation(MethodTree method) { + return (MethodInvocationTree) ((ReturnStatementTree) method.block().body().get(0)).expression(); + } + + private static Map records() { + Map result = new LinkedHashMap<>(); + result.put("schema", 1); + result.put("empty", capture("File.java", "")); + result.put("tokens", capture("A.java", "// header\r\nclass A { String s = \"😀\"; /* tail */ }\r\n// eof")); + result.put("semantics", capture("A.java", "import java.util.List; class A { T value; U id(U u) { return u; } String m() { return id(\"\"); } }")); + result.put("recovery", capture("A.java", "class A { Missing value; void m() { value.call(); } }")); + result.put("patterns", capture("A.java", "record A(int value) { int m(Object o) { return switch(o) { case A(int x) when x > 0 -> x; default -> 0; }; } }")); + result.put("module", capture("module-info.java", "module sample { requires java.base; exports sample; }")); + result.put("malformed", capture("A.java", "class A { void m( }")); + VisitorsBridgeForTests bridge = new VisitorsBridgeForTests.Builder(new ContractCheck()) + .enableSemanticWithProjectClasspath(JParserTestUtils.DEFAULT_CLASSPATH).build(); + JavaAstScanner.scanSingleFileForTests(TestUtils.inputFile("src/test/files/metrics/Classes.java"), bridge); + result.put("issues", bridge.testContexts().stream().map(FrontendContractSnapshot::issues).toList()); + return result; + } + + private static Map capture(String name, String source) { + JavaTree.CompilationUnitTreeImpl tree; + try { + tree = (JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(name, source, JParserTestUtils.DEFAULT_CLASSPATH); + } catch (RuntimeException failure) { + return Map.of("failure", failure.getClass().getName(), "message", failure.getMessage()); + } + try { + return new FrontendContractSnapshot().capture(tree); + } finally { + tree.sema.getEnvironmentCleaner().run(); + } + } + + private static class ContractCheck extends IssuableSubscriptionVisitor { + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.CLASS); + } + + @Override + public void visitNode(Tree tree) { + Tree name = ((ClassTree) tree).simpleName(); + ((org.sonar.java.reporting.InternalJavaIssueBuilder) ((org.sonar.java.testing.JavaFileScannerContextForTests) context).newIssue()).forRule(this).onTree(name).withMessage("Class declaration") + .withSecondaries(List.of(new org.sonar.plugins.java.api.JavaFileScannerContext.Location("Class body", tree))).withCost(2) + .withQuickFix(() -> JavaQuickFix.newQuickFix("Rename class").addTextEdit(JavaTextEdit.replaceTree(name, "Replacement")).build()).report(); + } + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/FrontendLifecycleTest.java b/java-frontend/src/test/java/org/sonar/java/model/FrontendLifecycleTest.java new file mode 100644 index 00000000000..bc19ff260b7 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/FrontendLifecycleTest.java @@ -0,0 +1,333 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import org.eclipse.core.runtime.OperationCanceledException; +import org.junit.jupiter.api.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.java.AnalysisProgress; +import org.sonar.java.ProgressMonitor; +import org.sonar.java.TestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class FrontendLifecycleTest { + + private static final FrontendConfiguration CONFIGURATION = FrontendConfiguration.from(JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, List.of(), false); + private static final InputFile INPUT = TestUtils.inputFile("src/test/files/metrics/Classes.java"); + + @Test + void cleanup_is_canonical_idempotent_and_attempts_every_resource() { + List calls = new ArrayList<>(); + RuntimeException first = new IllegalStateException("first"); + RuntimeException second = new IllegalArgumentException("second"); + FrontendResources resources = new FrontendResources(); + Runnable raw = () -> calls.add("once"); + Runnable handle = resources.own(raw); + assertThat(resources.own(raw)).isSameAs(handle).hasSameHashCodeAs(handle); + assertThat(new FrontendResources().own(raw)).isEqualTo(handle); + assertThat(handle).isNotEqualTo(resources.own(() -> calls.add("other"))).isNotNull().isNotEqualTo(raw); + resources.own(() -> { calls.add("first"); throw first; }); + resources.own(() -> { calls.add("second"); throw second; }); + resources.own(() -> calls.add("last")); + handle.run(); + assertThatThrownBy(resources::close).isSameAs(first).hasSuppressedException(second); + resources.close(); + handle.run(); + assertThat(calls).containsExactly("once", "other", "first", "second", "last"); + assertThatThrownBy(() -> resources.own(raw)).isInstanceOf(IllegalStateException.class); + } + + @Test + void cleanup_attempts_every_resource_and_suppresses_errors() { + FrontendResources resources = new FrontendResources(); + AtomicInteger cleaned = new AtomicInteger(); + AssertionError failure = new AssertionError("cleanup"); + IllegalStateException first = new IllegalStateException("first"); + resources.own(() -> { throw first; }); + resources.own(() -> { throw failure; }); + resources.own(cleaned::incrementAndGet); + + assertThatThrownBy(resources::close).isSameAs(first).hasSuppressedException(failure); + resources.close(); + assertThat(cleaned).hasValue(1); + } + + @Test + void cleanup_rethrows_first_error_after_attempting_every_resource() { + FrontendResources resources = new FrontendResources(); + AtomicInteger cleaned = new AtomicInteger(); + AssertionError first = new AssertionError("first"); + IllegalStateException second = new IllegalStateException("second"); + resources.own(() -> { throw first; }); + resources.own(() -> { throw first; }); + resources.own(() -> { throw second; }); + resources.own(cleaned::incrementAndGet); + + assertThatThrownBy(resources::close).isSameAs(first).hasSuppressedException(second); + resources.close(); + assertThat(cleaned).hasValue(1); + } + + @Test + void parse_failure_preserves_primary_and_suppresses_cleanup_failure() { + RuntimeException primary = new IllegalArgumentException("parse"); + RuntimeException secondary = new IllegalStateException("cleanup"); + StubSession session = new StubSession(); + session.failure = primary; + session.own(() -> { throw secondary; }); + assertThatThrownBy(() -> FrontendParser.parse(configuration -> session, CONFIGURATION, "A.java", "")) + .isSameAs(primary).hasSuppressedException(secondary); + assertThat(session.closed).isEqualTo(1); + session.close(); + assertThat(session.closed).isEqualTo(1); + } + + @Test + void parse_failure_suppresses_cleanup_errors() { + StubSession session = new StubSession(); + RuntimeException primary = new IllegalArgumentException("parse"); + session.failure = primary; + AssertionError failure = new AssertionError("cleanup"); + session.own(() -> { throw failure; }); + + assertThatThrownBy(() -> FrontendParser.parse(configuration -> session, CONFIGURATION, "A.java", "")) + .isSameAs(primary).hasSuppressedException(failure); + assertThat(session.closed).isEqualTo(1); + } + + @Test + void parse_errors_close_the_session_before_propagating() { + StubSession session = new StubSession(); + AssertionError failure = new AssertionError("parse"); + session.failure = failure; + try (session) { + assertThatThrownBy(() -> FrontendParser.parse(configuration -> session, CONFIGURATION, "A.java", "")) + .isSameAs(failure).hasNoSuppressedExceptions(); + assertThat(session.closed).isEqualTo(1); + } + assertThat(session.closed).isEqualTo(1); + } + + @Test + void parse_errors_preserve_primary_and_suppress_cleanup_failures() { + for (boolean cleanupError : List.of(false, true)) { + StubSession session = new StubSession(); + AssertionError primary = new AssertionError("parse"); + session.failure = primary; + Throwable secondary = cleanupError ? new AssertionError("cleanup") : new IllegalStateException("cleanup"); + session.own(() -> { + if (secondary instanceof Error error) { + throw error; + } + throw (RuntimeException) secondary; + }); + + assertThatThrownBy(() -> FrontendParser.parse(configuration -> session, CONFIGURATION, "A.java", "")) + .isSameAs(primary).hasSuppressedException(secondary); + assertThat(session.closed).isEqualTo(1); + } + } + + @Test + void conversion_failure_is_delivered_without_losing_session_ownership() { + StubSession session = new StubSession(); + RuntimeException conversion = new IllegalArgumentException("convert"); + session.conversionFailure = conversion; + List outcomes = new ArrayList<>(); + batch(configuration -> session).parse(List.of(INPUT), () -> false, new AnalysisProgress(1), (input, result) -> { + assertThat(session.closed).isZero(); + assertThatThrownBy(result::get).isSameAs(conversion); + outcomes.add(input.filename()); + }); + assertThat(outcomes).containsExactly(INPUT.filename()); + assertThat(session.closed).isEqualTo(1); + } + + @Test + void callback_failure_retries_pending_file_in_fresh_owned_session() { + StubSession first = new StubSession(); + StubSession fallback = new StubSession(); + AtomicInteger opened = new AtomicInteger(); + AtomicInteger callbacks = new AtomicInteger(); + batch(configuration -> opened.getAndIncrement() == 0 ? first : fallback) + .parse(List.of(INPUT), () -> false, new AnalysisProgress(1), (input, result) -> { + if (callbacks.getAndIncrement() == 0) { + throw new IllegalStateException("callback"); + } + assertThat(first.closed).isEqualTo(1); + assertThat(fallback.closed).isZero(); + }); + assertThat(opened).hasValue(2); + assertThat(callbacks).hasValue(2); + assertThat(fallback.closed).isEqualTo(1); + } + + @Test + void cancellation_and_error_before_first_result_close_without_fallback() { + for (Throwable failure : List.of(new OperationCanceledException("cancelled"), new AssertionError("before first result"))) { + StubSession session = new StubSession(); + session.failure = failure; + AtomicInteger opened = new AtomicInteger(); + assertThatThrownBy(() -> batch(configuration -> { opened.incrementAndGet(); return session; }) + .parse(List.of(INPUT), () -> false, new AnalysisProgress(1), (input, result) -> { throw new AssertionError("unexpected callback"); })) + .isSameAs(failure); + assertThat(opened).hasValue(1); + assertThat(session.closed).isEqualTo(1); + } + } + + @Test + void callback_error_preserves_primary_when_close_fails() { + StubSession session = new StubSession(); + Error primary = new AssertionError("callback"); + RuntimeException secondary = new IllegalStateException("close"); + session.own(() -> { throw secondary; }); + assertThatThrownBy(() -> batch(configuration -> session).parse(List.of(INPUT), () -> false, new AnalysisProgress(1), + (input, result) -> { throw primary; })).isSameAs(primary).hasSuppressedException(secondary); + assertThat(session.closed).isEqualTo(1); + } + + @Test + void ecj_registers_environment_before_syntax_conversion_failure() { + FrontendSession session = spy(new EcjFrontendBackend(() -> EcjFrontendBackend.newParser(CONFIGURATION)).open(CONFIGURATION)); + try (session) { + assertThatThrownBy(() -> session.parse("A.java", "class A { void m( }")) + .isInstanceOf(com.sonar.sslr.api.RecognitionException.class); + verify(session).own(any()); + } + } + + @Test + void overlapping_sessions_remain_alive_until_their_own_close() { + StubSession first = new StubSession(); + StubSession second = new StubSession(); + try (FrontendParser.ParsedFile a = FrontendParser.parse(configuration -> first, CONFIGURATION, "A.java", ""); + FrontendParser.ParsedFile b = FrontendParser.parse(configuration -> second, CONFIGURATION, "B.java", "")) { + a.close(); + assertThat(first.closed).isEqualTo(1); + assertThat(second.closed).isZero(); + assertThat(b.tree()).isSameAs(second.tree); + } + assertThat(first.closed).isEqualTo(1); + assertThat(second.closed).isEqualTo(1); + try (FrontendParser.ParsedFile a = FrontendParser.parse(JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, List.of(), "A.java", "class A {}"); + FrontendParser.ParsedFile b = FrontendParser.parse(JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, List.of(), "B.java", "class B { java.util.List value; }")) { + a.close(); + var type = (org.sonar.plugins.java.api.tree.ClassTree) b.tree().types().get(0); + var field = (org.sonar.plugins.java.api.tree.VariableTree) type.members().get(0); + assertThat(field.symbol().type().fullyQualifiedName()).isEqualTo("java.util.List"); + } + } + + @Test + void scanner_cleans_after_visitor_or_modification_failure_and_preserves_primary() { + for (boolean failModification : List.of(false, true)) { + AtomicInteger cleaned = new AtomicInteger(); + org.sonar.java.AnalysisException primary = new org.sonar.java.AnalysisException("scan"); + IllegalStateException secondary = new IllegalStateException("cleanup"); + VisitorsBridge visitor = mock(VisitorsBridge.class); + if (!failModification) { + doThrow(primary).when(visitor).visitFile(any(), org.mockito.ArgumentMatchers.anyBoolean()); + } + var scanner = new org.sonar.java.ast.JavaAstScanner(null, new org.sonar.java.telemetry.NoOpTelemetry(), + org.sonar.java.telemetry.TelemetryKey.JAVA_ANALYSIS_MAIN); + scanner.setVisitorBridge(visitor); + assertThatThrownBy(() -> JParserConfig.Mode.FILE_BY_FILE.create(JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, List.of()) + .parse(List.of(INPUT), () -> false, new AnalysisProgress(1), (input, result) -> scanner.simpleScan(input, result, + ast -> { cleaned.incrementAndGet(); throw secondary; }, ast -> { if (failModification) { throw primary; } }))) + .isSameAs(primary).hasSuppressedException(secondary); + assertThat(cleaned).hasValue(1); + } + } + + @Test + void batch_metadata_failures_propagate_before_opening_a_session_or_falling_back() { + for (boolean failPath : List.of(false, true)) { + InputFile input = mock(InputFile.class); + IllegalStateException failure = new IllegalStateException("input metadata"); + if (failPath) { + when(input.absolutePath()).thenThrow(failure); + } else { + when(input.absolutePath()).thenReturn("/tmp/A.java"); + when(input.charset()).thenThrow(failure); + } + AtomicInteger opened = new AtomicInteger(); + assertThatThrownBy(() -> batch(configuration -> { opened.incrementAndGet(); return new StubSession(); }) + .parse(List.of(input), () -> false, new AnalysisProgress(1), (file, result) -> { throw new AssertionError("unexpected callback"); })) + .isSameAs(failure); + assertThat(opened).hasValue(0); + } + } + + private static JParserConfig batch(FrontendBackend backend) { + return new JParserConfig.Batch(JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, List.of(), false) { + @Override + FrontendBackend backend() { + return backend; + } + }; + } + + private static final class StubSession extends FrontendSession { + private final JavaTree.CompilationUnitTreeImpl tree = mock(JavaTree.CompilationUnitTreeImpl.class); + private Throwable failure; + private RuntimeException conversionFailure; + private int closed; + + private StubSession() { + own(() -> closed++); + } + + @Override + JavaTree.CompilationUnitTreeImpl parse(String unitName, String source) { + failIfRequested(); + return tree; + } + + @Override + void parseBatch(FrontendBatch inputs, ProgressMonitor monitor, BiConsumer action) { + failIfRequested(); + inputs.inputs().forEach(input -> action.accept(input, () -> { + if (conversionFailure != null) { + throw conversionFailure; + } + return tree; + })); + } + + private void failIfRequested() { + if (failure instanceof RuntimeException exception) { + throw exception; + } + if (failure instanceof Error error) { + throw error; + } + } + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/JMethodSymbolTest.java b/java-frontend/src/test/java/org/sonar/java/model/JMethodSymbolTest.java index 6f0595798fd..bae9e77a15e 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JMethodSymbolTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JMethodSymbolTest.java @@ -35,6 +35,7 @@ import org.sonar.java.model.expression.NewClassTreeImpl; import org.sonar.java.model.statement.ExpressionStatementTreeImpl; import org.sonar.java.model.statement.ReturnStatementTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Type; @@ -54,11 +55,11 @@ import static org.mockito.Mockito.when; import static org.sonar.java.model.EcjTestSupport.methodBinding; import static org.sonar.java.model.EcjTestSupport.sema; -import static org.sonar.java.model.EcjTestSupport.typeBinding; import static org.sonar.java.model.JUtilsTest.firstClass; import static org.sonar.java.model.JUtilsTest.firstMethod; import static org.sonar.java.model.JUtilsTest.nthMethod; +@TestResourceScope.Scoped class JMethodSymbolTest { @Test @@ -66,15 +67,15 @@ void test() { JavaTree.CompilationUnitTreeImpl cu = test("class C { C m(C p) throws Exception { return null; } }"); ClassTreeImpl c = (ClassTreeImpl) cu.types().get(0); MethodTreeImpl m = (MethodTreeImpl) c.members().get(0); - JMethodSymbol symbol = sema(cu).methodSymbol(Objects.requireNonNull(methodBinding(m))); + Symbol.MethodSymbol symbol = m.symbol(); assertThat(symbol.parameterTypes()) - .containsOnly(sema(cu).type(Objects.requireNonNull(typeBinding(c)))); + .containsOnly(c.symbol().type()); assertThat(symbol.declarationParameters()) .containsExactly(m.parameters().get(0).symbol()); assertThat(symbol.returnType()) - .isSameAs(sema(cu).typeSymbol(Objects.requireNonNull(typeBinding(c)))); + .isSameAs(c.symbol()); assertThat(symbol.thrownTypes()) - .containsOnly(sema(cu).type(Objects.requireNonNull(sema(cu).resolveType("java.lang.Exception")))); + .containsOnly(cu.sema.getClassType("java.lang.Exception")); } @Test @@ -83,7 +84,7 @@ void test_with_generics() { ClassTreeImpl c = (ClassTreeImpl) cu.types().get(0); MethodTreeImpl m = (MethodTreeImpl) c.members().get(0); Symbol.MethodSymbol declarationSymbol = m.symbol(); - JType declarationParameterType = sema(cu).type(Objects.requireNonNull((methodBinding(m).getParameterTypes()[0]))); + Type declarationParameterType = m.parameters().get(0).type().symbolType(); assertThat(declarationSymbol.parameterTypes()) .containsOnly(declarationParameterType); Symbol declarationParameterSymbol = m.parameters().get(0).symbol(); diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserReleasingJarsTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserReleasingJarsTest.java index e9fb5846280..a927af0b0a0 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserReleasingJarsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserReleasingJarsTest.java @@ -23,6 +23,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.ExpressionStatementTree; @@ -31,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class JParserReleasingJarsTest { @TempDir diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java index 1c9e79b9aa1..e4e6822f61d 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java @@ -59,6 +59,7 @@ import org.sonar.java.model.statement.StaticInitializerTreeImpl; import org.sonar.java.model.statement.SwitchExpressionTreeImpl; import org.sonar.java.model.statement.YieldStatementTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.location.Position; import org.sonar.plugins.java.api.semantic.Symbol; @@ -109,6 +110,7 @@ import static org.sonar.java.model.assertions.TreeAssert.assertThat; import static org.sonar.java.model.assertions.TypeAssert.assertThat; +@TestResourceScope.Scoped class JParserSemanticTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserTest.java index 0ac663b4bcb..ca1818ce8b3 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserTest.java @@ -59,6 +59,7 @@ import org.sonar.java.TestUtils; import org.sonar.java.model.JavaTree.CompilationUnitTreeImpl; import org.sonar.java.model.declaration.ClassTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.testing.ThreadLocalLogTester; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.location.Range; @@ -106,6 +107,7 @@ import static org.sonar.java.model.JParserTestUtils.DEFAULT_CLASSPATH; import static org.sonar.java.model.JParserTestUtils.parse; +@TestResourceScope.Scoped class JParserTest { @RegisterExtension diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserTestUtils.java b/java-frontend/src/test/java/org/sonar/java/model/JParserTestUtils.java index 5224a7e09ec..97d0ca78696 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserTestUtils.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserTestUtils.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.test.classpath.TestClasspathUtils; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -81,7 +82,7 @@ public static CompilationUnitTree parse(String unitName, String source, List classpath, JavaVersion version) { - return JParser.parse(JParserConfig.Mode.FILE_BY_FILE.create(version, classpath).astParser(), version.toString(), unitName, source); + return TestResourceScope.retain(FrontendParser.parse(version, classpath, unitName, source)).tree(); } public static List checksTestClassPath() throws IOException { diff --git a/java-frontend/src/test/java/org/sonar/java/model/JSemaTest.java b/java-frontend/src/test/java/org/sonar/java/model/JSemaTest.java index ed51a20a6a9..1b524ad8858 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JSemaTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JSemaTest.java @@ -17,17 +17,16 @@ package org.sonar.java.model; import java.util.Objects; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.ITypeBinding; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Type; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +@TestResourceScope.Scoped class JSemaTest { @Test @@ -77,20 +76,7 @@ void resolvePackageAnnotations() { @BeforeEach void setup() { - ASTParser astParser = ASTParser.newParser(AST.getJLSLatest()); - String version = JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION.effectiveJavaVersionAsString(); - JavaCore.setComplianceOptions(version, JavaCore.getOptions()); - astParser.setEnvironment( - new String[]{"target/test-classes"}, - new String[]{}, - new String[]{}, - true - ); - astParser.setResolveBindings(true); - astParser.setUnitName("File.java"); - astParser.setSource("".toCharArray()); - AST ast = astParser.createAST(null).getAST(); - sema = new JSema(ast); + sema = EcjTestSupport.sema(SemanticFixture.scoped("", java.util.List.of(new java.io.File("target/test-classes"))).tree()); } } diff --git a/java-frontend/src/test/java/org/sonar/java/model/JSymbolMetadataTest.java b/java-frontend/src/test/java/org/sonar/java/model/JSymbolMetadataTest.java index 1597e9e24a5..b3ca625b88b 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JSymbolMetadataTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JSymbolMetadataTest.java @@ -30,6 +30,7 @@ import org.sonar.java.model.declaration.MethodTreeImpl; import org.sonar.java.model.expression.MethodInvocationTreeImpl; import org.sonar.java.model.statement.ExpressionStatementTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.SymbolMetadata.AnnotationInstance; @@ -55,6 +56,7 @@ import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType.UNKNOWN; import static org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType.WEAK_NULLABLE; +@TestResourceScope.Scoped class JSymbolMetadataTest { @Test @@ -83,12 +85,11 @@ void flattened_annotations_are_independent_of_exposed_metadata_collections() { @Test void should_convert_annotation_values() { - JavaTree.CompilationUnitTreeImpl cu = test("import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.METHOD}) @interface A { }"); - ClassTreeImpl c = (ClassTreeImpl) cu.types().get(0); - Object[] value = (Object[]) c.symbol().metadata().annotations().get(0).values().get(0).value(); + SemanticFixture fixture = SemanticFixture.scoped("import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.METHOD}) @interface A { }"); + Object[] value = (Object[]) fixture.annotations(fixture.declaration(0).symbol()).get(0).values().get(0).value(); assertThat(value).hasSize(2); - assertThat(((JVariableSymbol) value[0]).isEnum()).isTrue(); - assertThat(((JVariableSymbol) value[1]).isEnum()).isTrue(); + assertThat(((Symbol.VariableSymbol) value[0]).isEnum()).isTrue(); + assertThat(((Symbol.VariableSymbol) value[1]).isEnum()).isTrue(); } @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/JSymbolTest.java b/java-frontend/src/test/java/org/sonar/java/model/JSymbolTest.java index 64291977a35..bcf309e0e90 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JSymbolTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JSymbolTest.java @@ -29,6 +29,7 @@ import org.sonar.java.model.declaration.MethodTreeImpl; import org.sonar.java.model.declaration.VariableTreeImpl; import org.sonar.java.model.statement.BlockTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.Type; @@ -57,6 +58,7 @@ import static org.sonar.java.model.EcjTestSupport.variableBinding; import static org.sonar.java.model.assertions.SymbolAssert.assertThat; +@TestResourceScope.Scoped class JSymbolTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/JTypeSymbolTest.java b/java-frontend/src/test/java/org/sonar/java/model/JTypeSymbolTest.java index 654cbfada69..9c6ea3865d1 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JTypeSymbolTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JTypeSymbolTest.java @@ -22,6 +22,7 @@ import org.sonar.java.model.declaration.ClassTreeImpl; import org.sonar.java.model.declaration.MethodTreeImpl; import org.sonar.java.model.declaration.VariableTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Type; import static org.assertj.core.api.Assertions.assertThat; @@ -33,35 +34,22 @@ import static org.sonar.java.model.EcjTestSupport.typeBinding; import static org.sonar.java.model.EcjTestSupport.variableBinding; +@TestResourceScope.Scoped class JTypeSymbolTest { @Test void superClass() { - JavaTree.CompilationUnitTreeImpl cu = test("interface I { } class C implements I { } class C2 extends C { }"); - ITypeBinding javaLangObject = Objects.requireNonNull(sema(cu).resolveType("java.lang.Object")); - ITypeBinding i = Objects.requireNonNull(typeBinding(((ClassTreeImpl) cu.types().get(0)))); - ITypeBinding c = Objects.requireNonNull(typeBinding(((ClassTreeImpl) cu.types().get(1)))); - ITypeBinding c2 = Objects.requireNonNull(typeBinding(((ClassTreeImpl) cu.types().get(2)))); + SemanticFixture fixture = SemanticFixture.scoped("interface I { } class C implements I { } class C2 extends C { }"); + Type javaLangObject = fixture.type("java.lang.Object"); + var i = fixture.declaration(0).symbol(); + var c = fixture.declaration(1).symbol(); + var c2 = fixture.declaration(2).symbol(); assertAll( - // for java.lang.Object - () -> - assertThat(sema(cu).typeSymbol(javaLangObject).superClass()) - .isNull(), - // for interfaces - () -> - assertThat(sema(cu).typeSymbol(i).superClass()) - .isSameAs(sema(cu).type(javaLangObject)), - // for classes - () -> - assertThat(sema(cu).typeSymbol(c).superClass()) - .isSameAs(sema(cu).type(javaLangObject)), - () -> - assertThat(sema(cu).typeSymbol(c2).superClass()) - .isSameAs(sema(cu).type(c)), - // for arrays - () -> - assertThat(sema(cu).typeSymbol(javaLangObject.createArrayType(1)).superClass()) - .isSameAs(sema(cu).type(javaLangObject)) + () -> assertThat(javaLangObject.symbol().superClass()).isNull(), + () -> assertThat(i.superClass()).isSameAs(javaLangObject), + () -> assertThat(c.superClass()).isSameAs(javaLangObject), + () -> assertThat(c2.superClass()).isSameAs(c.type()), + () -> assertThat(fixture.type("java.lang.Object[]").symbol().superClass()).isSameAs(javaLangObject) ); } @@ -78,13 +66,11 @@ void testSuperClassWhenSemanticCanNotResolveObjectType() { @Test void interfaces() { - JavaTree.CompilationUnitTreeImpl cu = test("interface I { } class C implements I { }"); - ITypeBinding i = Objects.requireNonNull(typeBinding(((ClassTreeImpl) cu.types().get(0)))); - ITypeBinding c = Objects.requireNonNull(typeBinding(((ClassTreeImpl) cu.types().get(1)))); - assertThat(sema(cu).typeSymbol(i).interfaces()) - .isEmpty(); - assertThat(sema(cu).typeSymbol(c).interfaces()) - .containsOnly(sema(cu).type(i)); + SemanticFixture fixture = SemanticFixture.scoped("interface I { } class C implements I { }"); + var i = fixture.declaration(0).symbol(); + var c = fixture.declaration(1).symbol(); + assertThat(i.interfaces()).isEmpty(); + assertThat(c.interfaces()).containsOnly(i.type()); } @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/JTypeTest.java b/java-frontend/src/test/java/org/sonar/java/model/JTypeTest.java index 45352af96a1..979f2fc0802 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JTypeTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JTypeTest.java @@ -18,8 +18,6 @@ import java.util.Objects; import java.util.stream.Stream; -import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.ITypeBinding; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,6 +32,7 @@ import org.sonar.java.model.declaration.VariableTreeImpl; import org.sonar.java.model.statement.ExpressionStatementTreeImpl; import org.sonar.java.model.statement.ReturnStatementTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.testing.ThreadLocalLogTester; import org.sonar.plugins.java.api.semantic.Type; @@ -46,6 +45,7 @@ import static org.sonar.java.model.EcjTestSupport.typeBinding; import static org.sonar.java.model.assertions.TypeAssert.assertThat; +@TestResourceScope.Scoped class JTypeTest { @RegisterExtension @@ -276,7 +276,7 @@ void erasure() { @Test void elementType() { - assertThat(type("int[][]").elementType()) + assertThat(((Type.ArrayType) type("int[][]")).elementType()) .isSameAs(type("int[]")); } @@ -327,7 +327,7 @@ void null_type() { @Test void is_subtype_of_should_not_throw_NPE() { - JType objectType = type("java.lang.Object"); + Type objectType = type("java.lang.Object"); ITypeBinding brokenStringBinding = spy(Objects.requireNonNull(sema.resolveType("java.lang.String"))); // simulate the NullPointerException described in SONARJAVA-4390 when(brokenStringBinding.isSubTypeCompatible(any())) @@ -363,27 +363,17 @@ private static JavaTree.CompilationUnitTreeImpl test(String source) { return (JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source); } - private JType type(String name) { - ITypeBinding typeBinding = Objects.requireNonNull(sema.resolveType(name)); - return sema.type(typeBinding); + private Type type(String name) { + return fixture.type(name); } private JSema sema; + private SemanticFixture fixture; @BeforeEach void setup() { - ASTParser astParser = ASTParser.newParser(AST.getJLSLatest()); - astParser.setEnvironment( - new String[]{}, - new String[]{}, - new String[]{}, - true - ); - astParser.setResolveBindings(true); - astParser.setUnitName("File.java"); - astParser.setSource("".toCharArray()); - AST ast = astParser.createAST(null).getAST(); - sema = new JSema(ast); + fixture = SemanticFixture.scoped("", java.util.List.of()); + sema = sema(fixture.tree()); } } diff --git a/java-frontend/src/test/java/org/sonar/java/model/JUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/model/JUtilsTest.java index 16496a21611..0b73a544754 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JUtilsTest.java @@ -28,6 +28,7 @@ import org.sonar.java.model.expression.TypeCastExpressionTreeImpl; import org.sonar.java.model.statement.ExpressionStatementTreeImpl; import org.sonar.java.model.statement.ReturnStatementTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Sema; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; @@ -46,6 +47,7 @@ import static org.sonar.java.model.EcjTestSupport.typeBinding; import static org.sonar.java.model.EcjTestSupport.variableBinding; +@TestResourceScope.Scoped class JUtilsTest { static final JSema SEMA = sema(test("")); diff --git a/java-frontend/src/test/java/org/sonar/java/model/JVariableSymbolTest.java b/java-frontend/src/test/java/org/sonar/java/model/JVariableSymbolTest.java index cf569dcfa8a..c8759237c00 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JVariableSymbolTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JVariableSymbolTest.java @@ -20,15 +20,16 @@ import org.sonar.java.model.declaration.ClassTreeImpl; import org.sonar.java.model.declaration.MethodTreeImpl; import org.sonar.java.model.declaration.VariableTreeImpl; +import org.sonar.java.test.TestResourceScope; +import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.BlockTree; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.VariableTree; import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.java.model.EcjTestSupport.sema; -import static org.sonar.java.model.EcjTestSupport.variableBinding; +@TestResourceScope.Scoped class JVariableSymbolTest { @Test @@ -37,10 +38,10 @@ void isLocalVariable() { ClassTreeImpl c = (ClassTreeImpl) cu.types().get(0); MethodTreeImpl method = (MethodTreeImpl) c.members().get(0); VariableTreeImpl localVariable = (VariableTreeImpl) method.block().body().get(0); - JVariableSymbol variableSymbol = sema(cu).variableSymbol(variableBinding(localVariable)); + Symbol variableSymbol = localVariable.symbol(); assertThat(variableSymbol.isLocalVariable()).isTrue(); VariableTreeImpl classVariable = (VariableTreeImpl) c.members().get(1); - variableSymbol = sema(cu).variableSymbol(variableBinding(classVariable)); + variableSymbol = classVariable.symbol(); assertThat(variableSymbol.isLocalVariable()).isFalse(); } @@ -63,15 +64,15 @@ void isParameter() { MethodTreeImpl method = (MethodTreeImpl) c.members().get(0); VariableTreeImpl localVariable = (VariableTreeImpl) method.block().body().get(0); - JVariableSymbol variableSymbol = sema(cu).variableSymbol(variableBinding(localVariable)); + Symbol variableSymbol = localVariable.symbol(); assertThat(variableSymbol.isParameter()).isFalse(); VariableTreeImpl parameter = (VariableTreeImpl) method.parameters().get(0); - variableSymbol = sema(cu).variableSymbol(variableBinding(parameter)); + variableSymbol = parameter.symbol(); assertThat(variableSymbol.isParameter()).isTrue(); } private static JavaTree.CompilationUnitTreeImpl test(String source) { - return (JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source); + return SemanticFixture.scoped(source).tree(); } } diff --git a/java-frontend/src/test/java/org/sonar/java/model/JavaTreeModelTest.java b/java-frontend/src/test/java/org/sonar/java/model/JavaTreeModelTest.java index 45b813fc693..926c7b3012d 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JavaTreeModelTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JavaTreeModelTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.sonar.java.ast.parser.TypeParameterListTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.AnnotationTree; import org.sonar.plugins.java.api.tree.Arguments; import org.sonar.plugins.java.api.tree.ArrayAccessExpressionTree; @@ -88,6 +89,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.java.model.assertions.TreeAssert.assertThat; +@TestResourceScope.Scoped class JavaTreeModelTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/KeywordSuperTest.java b/java-frontend/src/test/java/org/sonar/java/model/KeywordSuperTest.java index f1ef8a10b4c..776bc2be775 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/KeywordSuperTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/KeywordSuperTest.java @@ -17,6 +17,7 @@ package org.sonar.java.model; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; @@ -26,6 +27,7 @@ import static org.sonar.java.model.EcjTestSupport.clearSymbol; import static org.sonar.java.model.EcjTestSupport.clearType; +@TestResourceScope.Scoped class KeywordSuperTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/KeywordThisTest.java b/java-frontend/src/test/java/org/sonar/java/model/KeywordThisTest.java index c9a9d639565..8d634104562 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/KeywordThisTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/KeywordThisTest.java @@ -17,6 +17,7 @@ package org.sonar.java.model; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; @@ -26,6 +27,7 @@ import static org.sonar.java.model.EcjTestSupport.clearSymbol; import static org.sonar.java.model.EcjTestSupport.clearType; +@TestResourceScope.Scoped class KeywordThisTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/LineUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/model/LineUtilsTest.java index abdf7a2673b..8a8c2845fab 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/LineUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/LineUtilsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.SyntaxToken; @@ -28,6 +29,7 @@ import static org.sonar.java.model.LineUtils.splitLines; import static org.sonar.java.model.LineUtils.startLine; +@TestResourceScope.Scoped class LineUtilsTest { private static CompilationUnitTree tree; diff --git a/java-frontend/src/test/java/org/sonar/java/model/LiteralUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/model/LiteralUtilsTest.java index 24f17cad1ab..6e80891fc13 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/LiteralUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/LiteralUtilsTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -38,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class LiteralUtilsTest { static List variables; diff --git a/java-frontend/src/test/java/org/sonar/java/model/ModifiersUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/model/ModifiersUtilsTest.java index 4126d8e4bf3..d2e22fc0f92 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/ModifiersUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/ModifiersUtilsTest.java @@ -19,6 +19,7 @@ import java.io.File; import java.lang.reflect.Constructor; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.Modifier; @@ -29,6 +30,7 @@ import static java.lang.reflect.Modifier.isPrivate; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class ModifiersUtilsTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/SemanticFixture.java b/java-frontend/src/test/java/org/sonar/java/model/SemanticFixture.java new file mode 100644 index 00000000000..09b580fed9b --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/SemanticFixture.java @@ -0,0 +1,78 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.io.File; +import java.util.List; +import org.sonar.java.test.TestResourceScope; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.SymbolMetadata; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.VariableTree; + +/** Backend-aware source fixtures expose Sonar declarations and retain the parser through lazy queries. */ +final class SemanticFixture implements AutoCloseable { + + private final FrontendParser.ParsedFile parsed; + + private SemanticFixture(FrontendParser.ParsedFile parsed) { + this.parsed = parsed; + } + + static SemanticFixture scoped(String source) { + return scoped(source, JParserTestUtils.DEFAULT_CLASSPATH); + } + + static SemanticFixture scoped(String source, List classpath) { + return TestResourceScope.retain(new SemanticFixture(FrontendParser.parse(JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, classpath, "File.java", source))); + } + + static SemanticFixture open(FrontendBackend backend, FrontendConfiguration configuration, String unitName, String source) { + return new SemanticFixture(FrontendParser.parse(backend, configuration, unitName, source)); + } + + JavaTree.CompilationUnitTreeImpl tree() { + return parsed.tree(); + } + + ClassTree declaration(int index) { + return (ClassTree) tree().types().get(index); + } + + MethodTree method(int typeIndex, int memberIndex) { + return (MethodTree) declaration(typeIndex).members().get(memberIndex); + } + + VariableTree variable(int typeIndex, int memberIndex) { + return (VariableTree) declaration(typeIndex).members().get(memberIndex); + } + + Type type(String name) { + return tree().sema.getClassType(name); + } + + List annotations(Symbol symbol) { + return symbol.metadata().annotations(); + } + + @Override + public void close() { + parsed.close(); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/SemanticFixtureTest.java b/java-frontend/src/test/java/org/sonar/java/model/SemanticFixtureTest.java new file mode 100644 index 00000000000..1f23503b3c7 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/SemanticFixtureTest.java @@ -0,0 +1,95 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.io.File; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.sonar.java.test.TestResourceScope; +import org.sonar.plugins.java.api.tree.ClassTree; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@TestResourceScope.Scoped +class SemanticFixtureTest { + + @Test + void fixtures_expose_declarations_types_metadata_defaults_and_recovery() { + SemanticFixture fixture = SemanticFixture.scoped("@interface A { String value() default \"default\"; } @A class C { Missing missing; int field; void m() {} }"); + assertThat(fixture.method(0, 0).symbol().defaultValue()).isEqualTo("default"); + assertThat(fixture.annotations(fixture.declaration(1).symbol())).hasSize(1); + assertThat(fixture.annotations(fixture.declaration(1).symbol()).get(0).symbol().name()).isEqualTo("A"); + assertThat(fixture.variable(1, 0).symbol().type().isUnknown()).isTrue(); + assertThat(fixture.variable(1, 1).symbol().type()).isSameAs(fixture.type("int")); + assertThat(fixture.method(1, 2).symbol().declaration()).isSameAs(fixture.method(1, 2)); + assertThat(fixture.type("java.lang.String")).isSameAs(fixture.type("java.lang.String")); + } + + @Test + void injected_backend_failure_closes_resources_and_preserves_failure() { + AtomicInteger cleaned = new AtomicInteger(); + IllegalStateException failure = new IllegalStateException("injected backend"); + FrontendBackend backend = configuration -> new FrontendSession() { + { + own(cleaned::incrementAndGet); + } + + @Override + JavaTree.CompilationUnitTreeImpl parse(String unitName, String source) { + throw failure; + } + + @Override + void parseBatch(FrontendBatch inputs, org.sonar.java.ProgressMonitor monitor, + java.util.function.BiConsumer action) { + throw new AssertionError("unexpected batch parse"); + } + }; + assertThatThrownBy(() -> SemanticFixture.open(backend, + FrontendConfiguration.from(JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, List.of(), false), "A.java", "class A {}")) + .isSameAs(failure); + assertThat(cleaned).hasValue(1); + } + + @Test + void helper_preserves_string_input_and_existing_file_line_normalization(@TempDir java.nio.file.Path directory) throws Exception { + String source = "class A {\r\n String value = \"😀\";\r\n}\r\n"; + var direct = (JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source); + assertThat(direct.eofToken().range().start().line()).isEqualTo(4); + java.nio.file.Path file = directory.resolve("A.java"); + java.nio.file.Files.writeString(file, source); + var fromFile = (JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(file.toFile()); + assertThat(fromFile.eofToken().range().start().line()).isEqualTo(3); + assertThat(((ClassTree) direct.types().get(0)).symbol().name()).isEqualTo(((ClassTree) fromFile.types().get(0)).symbol().name()); + } + + @Test + void effective_configuration_is_immutable_and_keeps_classpath_order() { + var classpath = new java.util.ArrayList<>(List.of(new File("first"), new File("second"))); + FrontendConfiguration configuration = FrontendConfiguration.from(new JavaVersionImpl(17, true), classpath, true); + classpath.clear(); + assertThat(configuration.classpath()).containsExactly(new File("first"), new File("second")); + List configuredClasspath = configuration.classpath(); + assertThatThrownBy(configuredClasspath::clear).isInstanceOf(UnsupportedOperationException.class); + assertThat(configuration.sourceVersion()).isEqualTo("17"); + assertThat(configuration.previewEnabled()).isTrue(); + assertThat(configuration.ignoreUnnamedModuleForSplitPackage()).isTrue(); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/SymbolsTest.java b/java-frontend/src/test/java/org/sonar/java/model/SymbolsTest.java index cf0d6e33345..7c7e42e7b1e 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/SymbolsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/SymbolsTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.declaration.ClassTreeImpl; import org.sonar.java.model.declaration.VariableTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.SymbolMetadata.AnnotationInstance; @@ -37,6 +38,7 @@ import static org.mockito.Mockito.when; import static org.sonar.java.model.EcjTestSupport.sema; +@TestResourceScope.Scoped class SymbolsTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/SyntacticEquivalenceTest.java b/java-frontend/src/test/java/org/sonar/java/model/SyntacticEquivalenceTest.java index 440839a0cfb..1785760006c 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/SyntacticEquivalenceTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/SyntacticEquivalenceTest.java @@ -21,6 +21,7 @@ import java.util.List; import org.assertj.core.api.AbstractBooleanAssert; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ExpressionStatementTree; @@ -32,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class SyntacticEquivalenceTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/TypeUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/model/TypeUtilsTest.java index 919a8ee5353..33a6ff6753c 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/TypeUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/TypeUtilsTest.java @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import org.junit.jupiter.api.Test; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.VariableTree; @@ -27,6 +28,7 @@ import static java.lang.reflect.Modifier.isPrivate; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class TypeUtilsTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/VisitorsBridgeTest.java b/java-frontend/src/test/java/org/sonar/java/model/VisitorsBridgeTest.java index 76df3e194d3..e02431ed82b 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/VisitorsBridgeTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/VisitorsBridgeTest.java @@ -43,6 +43,7 @@ import org.sonar.java.checks.VisitorThatCanBeSkipped; import org.sonar.java.exceptions.ApiMismatchException; import org.sonar.java.notchecks.VisitorNotInChecksPackage; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.testing.ThreadLocalLogTester; import org.sonar.plugins.java.api.DependencyVersionAware; import org.sonar.plugins.java.api.InputFileScannerContext; @@ -74,6 +75,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +@TestResourceScope.Scoped class VisitorsBridgeTest { @RegisterExtension @@ -83,7 +85,7 @@ class VisitorsBridgeTest { private static final File FILE = new File("src/test/files/model/SimpleClass.java"); private static final InputFile INPUT_FILE = TestUtils.inputFile(FILE); - private static final CompilationUnitTree COMPILATION_UNIT_TREE = JParserTestUtils.parse(FILE); + private final CompilationUnitTree compilationUnitTree = JParserTestUtils.parse(FILE); private static final NullPointerException NPE = new NullPointerException("BimBadaboum"); @@ -91,7 +93,7 @@ class VisitorsBridgeTest { void rethrow_exception_when_hidden_property_set_to_true_with_JavaFileScanner() { VisitorsBridge visitorsBridge = visitorsBridge(new JFS_ThrowingNPEJavaFileScanner(), true); - assertThatThrownBy(() -> visitorsBridge.visitFile(COMPILATION_UNIT_TREE, false)) + assertThatThrownBy(() -> visitorsBridge.visitFile(compilationUnitTree, false)) .isInstanceOf(AnalysisException.class) .hasMessageContaining("Failing check") .hasCauseInstanceOf(CheckFailureException.class) @@ -106,7 +108,7 @@ void rethrow_exception_when_hidden_property_set_to_true_with_JavaFileScanner() { void swallow_exception_when_hidden_property_set_to_false_with_JavaFileScanner() { assertThatCode(() -> visitorsBridge(new JFS_ThrowingNPEJavaFileScanner(), false) - .visitFile(COMPILATION_UNIT_TREE, false) + .visitFile(compilationUnitTree, false) ) .withFailMessage("Exception should be swallowed when property is not set") .doesNotThrowAnyException(); @@ -120,7 +122,7 @@ void swallow_exception_when_hidden_property_set_to_false_with_JavaFileScanner() void rethrow_exception_when_hidden_property_set_to_true_with_SubscriptionVisitor() { VisitorsBridge visitorsBridge = visitorsBridge(new SV1_ThrowingNPEVisitingClass(), true); - assertThatThrownBy(() -> visitorsBridge.visitFile(COMPILATION_UNIT_TREE, false)) + assertThatThrownBy(() -> visitorsBridge.visitFile(compilationUnitTree, false)) .isInstanceOf(AnalysisException.class) .hasMessageContaining("Failing check") .hasCauseInstanceOf(CheckFailureException.class) @@ -140,7 +142,7 @@ void swallow_exception_when_hidden_property_set_to_false_with_SubscriptionVisito new SV3_ThrowingNPEVisitingToken(), new SV4_ThrowingNPEVisitingTrivia()), false) - .visitFile(COMPILATION_UNIT_TREE, false); + .visitFile(compilationUnitTree, false); }) .withFailMessage("Exceptions should be swallowed when property is not set") .doesNotThrowAnyException(); @@ -161,7 +163,7 @@ void swallow_exception_when_hidden_property_set_to_false_with_IssuableSubscripti new IV1_ThrowingNPEVisitingClass(), new IV2_ThrowingNPELeavingClass()), false) - .visitFile(COMPILATION_UNIT_TREE, false); + .visitFile(compilationUnitTree, false); }) .withFailMessage("Exceptions should be swallowed when property is not set") .doesNotThrowAnyException(); @@ -175,7 +177,7 @@ void swallow_exception_when_hidden_property_set_to_false_with_IssuableSubscripti void clear_state_when_leave_file_does_not_delegate() { StatefulVisitorWithoutLeaveFileDelegation visitor = new StatefulVisitorWithoutLeaveFileDelegation(false); - visitorsBridge(visitor, false).visitFile(COMPILATION_UNIT_TREE, false); + visitorsBridge(visitor, false).visitFile(compilationUnitTree, false); assertThat(visitor.state).isFalse(); assertThat(visitor.leaveCalled).isTrue(); @@ -188,7 +190,7 @@ void clear_state_when_visit_fails_before_leave_file() { VisitorsBridge visitorsBridge = visitorsBridge(visitor, true); - assertThatThrownBy(() -> visitorsBridge.visitFile(COMPILATION_UNIT_TREE, false)) + assertThatThrownBy(() -> visitorsBridge.visitFile(compilationUnitTree, false)) .isInstanceOf(AnalysisException.class); assertThat(visitor.state).isFalse(); @@ -206,11 +208,11 @@ void clear_all_visitors_state_when_cleanup_fails(boolean failOnException) { VisitorsBridge visitorsBridge = visitorsBridge(List.of(firstVisitor, secondVisitor, lastVisitor), failOnException); if (failOnException) { - assertThatThrownBy(() -> visitorsBridge.visitFile(COMPILATION_UNIT_TREE, false)) + assertThatThrownBy(() -> visitorsBridge.visitFile(compilationUnitTree, false)) .isInstanceOf(AnalysisException.class) .hasRootCause(firstFailure); } else { - assertThatCode(() -> visitorsBridge.visitFile(COMPILATION_UNIT_TREE, false)).doesNotThrowAnyException(); + assertThatCode(() -> visitorsBridge.visitFile(compilationUnitTree, false)).doesNotThrowAnyException(); } assertThat(lastVisitor.state).isFalse(); @@ -228,7 +230,7 @@ void cleanup_failure_does_not_mask_visit_failure() { ThrowingCleanupVisitor cleanupVisitor = new ThrowingCleanupVisitor(new IllegalStateException("Cleanup failure")); VisitorsBridge visitorsBridge = visitorsBridge(List.of(failingVisitor, cleanupVisitor), true); - assertThatThrownBy(() -> visitorsBridge.visitFile(COMPILATION_UNIT_TREE, false)) + assertThatThrownBy(() -> visitorsBridge.visitFile(compilationUnitTree, false)) .isInstanceOf(AnalysisException.class) .hasRootCause(NPE); @@ -243,7 +245,7 @@ void swallow_exception_when_hidden_property_set_to_false_with_all_kinds_of_visis new SV1_ThrowingNPEVisitingClass(), new IV1_ThrowingNPEVisitingClass()), false) - .visitFile(COMPILATION_UNIT_TREE, false); + .visitFile(compilationUnitTree, false); }) .withFailMessage("Exceptions should be swallowed when property is not set") .doesNotThrowAnyException(); @@ -258,7 +260,7 @@ void swallow_exception_when_hidden_property_set_to_false_with_all_kinds_of_visis @Test void no_log_when_filter_execute_fine() { VisitorsBridge visitorsBridge = visitorsBridge(Arrays.asList(), true); - assertThatCode(() -> visitorsBridge.visitFile(COMPILATION_UNIT_TREE, false)) + assertThatCode(() -> visitorsBridge.visitFile(compilationUnitTree, false)) .doesNotThrowAnyException(); } diff --git a/java-frontend/src/test/java/org/sonar/java/model/declaration/ClassTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/declaration/ClassTreeImplTest.java index 4e811aefdaf..4a8660d3949 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/declaration/ClassTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/declaration/ClassTreeImplTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.JavaTree; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -32,6 +33,7 @@ import static org.sonar.java.model.EcjTestSupport.clearType; import static org.sonar.java.model.assertions.TreeAssert.assertThat; +@TestResourceScope.Scoped class ClassTreeImplTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/declaration/ExportsDirectiveTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/declaration/ExportsDirectiveTreeImplTest.java index 8d7214bfc1b..3ffd096b673 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/declaration/ExportsDirectiveTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/declaration/ExportsDirectiveTreeImplTest.java @@ -20,6 +20,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ExportsDirectiveTree; @@ -33,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class ExportsDirectiveTreeImplTest { private static ExportsDirectiveTree exportsDirective(String exportsDirective) { diff --git a/java-frontend/src/test/java/org/sonar/java/model/declaration/MethodTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/declaration/MethodTreeImplTest.java index 6ee04398295..c754dd30a01 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/declaration/MethodTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/declaration/MethodTreeImplTest.java @@ -19,6 +19,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.cfg.ControlFlowGraph; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.ClassTree; @@ -31,6 +32,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +@TestResourceScope.Scoped class MethodTreeImplTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/declaration/ModuleDeclarationTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/declaration/ModuleDeclarationTreeImplTest.java index 4cd967d2750..2039df27156 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/declaration/ModuleDeclarationTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/declaration/ModuleDeclarationTreeImplTest.java @@ -19,6 +19,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.AnnotationTree; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class ModuleDeclarationTreeImplTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/declaration/OpensDirectiveTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/declaration/OpensDirectiveTreeImplTest.java index 9db0455d167..ba3b6fe39f3 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/declaration/OpensDirectiveTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/declaration/OpensDirectiveTreeImplTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ExportsDirectiveTree; @@ -34,6 +35,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class OpensDirectiveTreeImplTest { private static OpensDirectiveTree moduleDirective(String exportsDirective) { diff --git a/java-frontend/src/test/java/org/sonar/java/model/declaration/ProvidesDirectiveTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/declaration/ProvidesDirectiveTreeImplTest.java index 42270e808be..10ceb2a3e1b 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/declaration/ProvidesDirectiveTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/declaration/ProvidesDirectiveTreeImplTest.java @@ -20,6 +20,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ExportsDirectiveTree; @@ -32,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class ProvidesDirectiveTreeImplTest { private static ProvidesDirectiveTree providesDirective(String exportsDirective) { diff --git a/java-frontend/src/test/java/org/sonar/java/model/declaration/RequiresDirectiveTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/declaration/RequiresDirectiveTreeImplTest.java index 3a28b9419d0..0fdb0c46685 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/declaration/RequiresDirectiveTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/declaration/RequiresDirectiveTreeImplTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.ModifiersUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.IdentifierTree; @@ -32,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class RequiresDirectiveTreeImplTest { private static RequiresDirectiveTree requireDirective(String requiresDirective) { diff --git a/java-frontend/src/test/java/org/sonar/java/model/declaration/UsesDirectiveTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/declaration/UsesDirectiveTreeImplTest.java index ba2d4a587e9..9fb0abd2565 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/declaration/UsesDirectiveTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/declaration/UsesDirectiveTreeImplTest.java @@ -20,6 +20,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.IdentifierTree; @@ -30,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class UsesDirectiveTreeImplTest { private static UsesDirectiveTree moduleDirective(String exportsDirective) { diff --git a/java-frontend/src/test/java/org/sonar/java/model/expression/AssessableExpressionTreeTest.java b/java-frontend/src/test/java/org/sonar/java/model/expression/AssessableExpressionTreeTest.java index 96bf27c54f4..2650c813cfc 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/expression/AssessableExpressionTreeTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/expression/AssessableExpressionTreeTest.java @@ -22,6 +22,7 @@ import java.util.function.Function; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.ExpressionStatementTree; @@ -33,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class AssessableExpressionTreeTest { private final ClassTree classTree = parse(); diff --git a/java-frontend/src/test/java/org/sonar/java/model/expression/InstanceOfTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/expression/InstanceOfTreeImplTest.java index ae022081881..353231813b2 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/expression/InstanceOfTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/expression/InstanceOfTreeImplTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ClassTree; @@ -38,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.java.model.assertions.TreeAssert.assertThat; +@TestResourceScope.Scoped class InstanceOfTreeImplTest { private static final String CLASS_WITH_INSTANCE_OF = """ diff --git a/java-frontend/src/test/java/org/sonar/java/model/expression/LambdaExpressionTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/expression/LambdaExpressionTreeImplTest.java index 34cd80a1ce0..0f521f53131 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/expression/LambdaExpressionTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/expression/LambdaExpressionTreeImplTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.cfg.ControlFlowGraph; import org.sonar.plugins.java.api.cfg.ControlFlowGraph.Block; import org.sonar.plugins.java.api.semantic.Symbol; @@ -38,6 +39,7 @@ import static org.sonar.java.model.assertions.SymbolAssert.assertThat; import static org.sonar.java.model.assertions.TypeAssert.assertThat; +@TestResourceScope.Scoped class LambdaExpressionTreeImplTest { private static final String CLASS_WITH_METHOD_WITH_LAMBDA = """ diff --git a/java-frontend/src/test/java/org/sonar/java/model/expression/MethodInvocationTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/expression/MethodInvocationTreeImplTest.java index bf73dc9c131..c2654634dc5 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/expression/MethodInvocationTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/expression/MethodInvocationTreeImplTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class MethodInvocationTreeImplTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/pattern/AbstractPatternTreeTest.java b/java-frontend/src/test/java/org/sonar/java/model/pattern/AbstractPatternTreeTest.java index 55927df29f4..9fe50733bdd 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/pattern/AbstractPatternTreeTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/pattern/AbstractPatternTreeTest.java @@ -26,6 +26,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.JavaTree; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.CaseGroupTree; @@ -52,6 +53,7 @@ import static org.sonar.java.model.assertions.TreeAssert.assertThat; import static org.sonar.java.model.assertions.TypeAssert.assertThat; +@TestResourceScope.Scoped class AbstractPatternTreeTest { private static final String BASE_SOURCE_CODE = """ diff --git a/java-frontend/src/test/java/org/sonar/java/model/pattern/RecordPatternTreeImplTest.java b/java-frontend/src/test/java/org/sonar/java/model/pattern/RecordPatternTreeImplTest.java index fe00ce5c424..98245bf2d14 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/pattern/RecordPatternTreeImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/pattern/RecordPatternTreeImplTest.java @@ -19,12 +19,14 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.location.InternalPosition; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.SwitchStatementTree; import static org.junit.jupiter.api.Assertions.assertEquals; +@TestResourceScope.Scoped class RecordPatternTreeImplTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java b/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java index 950640e21c5..df5227fad7c 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java @@ -34,6 +34,7 @@ import org.sonar.api.batch.sensor.cache.WriteCache; import org.sonar.java.TestUtils; import org.sonar.java.telemetry.NoOpTelemetry; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.test.classpath.TestClasspathUtils; import org.sonar.plugins.java.api.InputFileScannerContext; import org.sonar.plugins.java.api.caching.CacheContext; @@ -47,6 +48,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped class BeanDefinitionGathererTest extends SpringContextGathererTest { @BeforeEach diff --git a/java-frontend/src/test/java/org/sonar/java/model/springcontext/ComponentScanPackageGathererTest.java b/java-frontend/src/test/java/org/sonar/java/model/springcontext/ComponentScanPackageGathererTest.java index bcc6db3ce2d..d6f010ac204 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/springcontext/ComponentScanPackageGathererTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/springcontext/ComponentScanPackageGathererTest.java @@ -27,6 +27,7 @@ import org.sonar.api.batch.sensor.cache.WriteCache; import org.sonar.java.TestUtils; import org.sonar.java.telemetry.NoOpTelemetry; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.InputFileScannerContext; import org.sonar.plugins.java.api.caching.CacheContext; import org.sonar.plugins.java.api.caching.JavaReadCache; @@ -39,6 +40,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped class ComponentScanPackageGathererTest extends SpringContextGathererTest { private static final String MODULE_KEY = ""; diff --git a/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextGathererTest.java b/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextGathererTest.java index 8c8e4258c4e..d955766766c 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextGathererTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextGathererTest.java @@ -25,6 +25,7 @@ import org.sonar.java.TestUtils; import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.VisitorsBridge; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.test.classpath.TestClasspathUtils; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.caching.CacheContext; @@ -36,6 +37,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped abstract class SpringContextGathererTest { protected SpringContextModelGatherer gatherer; diff --git a/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextModelGathererTest.java b/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextModelGathererTest.java index 5c0edb93347..bdc48567f9a 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextModelGathererTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/springcontext/SpringContextModelGathererTest.java @@ -34,6 +34,7 @@ import org.sonar.java.telemetry.DefaultTelemetry; import org.sonar.java.telemetry.NoOpTelemetry; import org.sonar.java.telemetry.Telemetry; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.test.classpath.TestClasspathUtils; import org.sonar.plugins.java.api.InputFileScannerContext; import org.sonar.plugins.java.api.JavaCheck; @@ -43,6 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +@TestResourceScope.Scoped class SpringContextModelGathererTest { private static final String GATHERING_TIME_KEY = "java.spring.context_model_gathering_time_ms"; diff --git a/java-frontend/src/test/java/org/sonar/java/regex/JavaAnalyzerRegexSourceTest.java b/java-frontend/src/test/java/org/sonar/java/regex/JavaAnalyzerRegexSourceTest.java index 0c1368a8218..31a27d1d20b 100644 --- a/java-frontend/src/test/java/org/sonar/java/regex/JavaAnalyzerRegexSourceTest.java +++ b/java-frontend/src/test/java/org/sonar/java/regex/JavaAnalyzerRegexSourceTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.reporting.AnalyzerMessage; import org.sonar.java.reporting.AnalyzerMessage.TextSpan; +import org.sonar.java.test.TestResourceScope; import org.sonarsource.analyzer.commons.regex.RegexParseResult; import org.sonarsource.analyzer.commons.regex.RegexParser; import org.sonarsource.analyzer.commons.regex.RegexSource; @@ -38,6 +39,7 @@ import static org.sonar.java.regex.RegexParserTestUtils.assertKind; import static org.sonar.java.regex.RegexParserTestUtils.assertSuccessfulParse; +@TestResourceScope.Scoped class JavaAnalyzerRegexSourceTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/regex/RegexCacheTest.java b/java-frontend/src/test/java/org/sonar/java/regex/RegexCacheTest.java index dc39d9f6903..b8d0b6f2909 100644 --- a/java-frontend/src/test/java/org/sonar/java/regex/RegexCacheTest.java +++ b/java-frontend/src/test/java/org/sonar/java/regex/RegexCacheTest.java @@ -19,6 +19,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.LiteralTree; @@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class RegexCacheTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/regex/RegexCheckTest.java b/java-frontend/src/test/java/org/sonar/java/regex/RegexCheckTest.java index f0918a4d005..9edf689b192 100644 --- a/java-frontend/src/test/java/org/sonar/java/regex/RegexCheckTest.java +++ b/java-frontend/src/test/java/org/sonar/java/regex/RegexCheckTest.java @@ -20,6 +20,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.reporting.AnalyzerMessage.TextSpan; +import org.sonar.java.test.TestResourceScope; import org.sonarsource.analyzer.commons.regex.ast.CharacterTree; import org.sonarsource.analyzer.commons.regex.ast.RegexSyntaxElement; import org.sonarsource.analyzer.commons.regex.ast.RegexTree; @@ -29,6 +30,7 @@ import static org.sonar.java.regex.RegexParserTestUtils.assertKind; import static org.sonar.java.regex.RegexParserTestUtils.assertSuccessfulParse; +@TestResourceScope.Scoped class RegexCheckTest implements RegexCheck { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/reporting/AnalyzerMessageTest.java b/java-frontend/src/test/java/org/sonar/java/reporting/AnalyzerMessageTest.java index 699c66b8897..ac19fe87482 100644 --- a/java-frontend/src/test/java/org/sonar/java/reporting/AnalyzerMessageTest.java +++ b/java-frontend/src/test/java/org/sonar/java/reporting/AnalyzerMessageTest.java @@ -21,6 +21,7 @@ import org.sonar.java.TestUtils; import org.sonar.java.model.JParserTestUtils; import org.sonar.java.reporting.AnalyzerMessage.TextSpan; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -38,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; +@TestResourceScope.Scoped class AnalyzerMessageTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/reporting/InternalJavaIssueBuilderTest.java b/java-frontend/src/test/java/org/sonar/java/reporting/InternalJavaIssueBuilderTest.java index 9afb8aea25a..78cbefe16a1 100644 --- a/java-frontend/src/test/java/org/sonar/java/reporting/InternalJavaIssueBuilderTest.java +++ b/java-frontend/src/test/java/org/sonar/java/reporting/InternalJavaIssueBuilderTest.java @@ -52,6 +52,7 @@ import org.sonar.java.SonarComponents; import org.sonar.java.TestUtils; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.testing.ThreadLocalLogTester; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.JavaFileScannerContext; @@ -75,6 +76,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +@TestResourceScope.Scoped class InternalJavaIssueBuilderTest { @RegisterExtension diff --git a/java-frontend/src/test/java/org/sonar/java/reporting/JavaTextEditTest.java b/java-frontend/src/test/java/org/sonar/java/reporting/JavaTextEditTest.java index 8fb438ab222..244d3c82899 100644 --- a/java-frontend/src/test/java/org/sonar/java/reporting/JavaTextEditTest.java +++ b/java-frontend/src/test/java/org/sonar/java/reporting/JavaTextEditTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.expression.LiteralTreeImpl; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.LiteralTree; @@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@TestResourceScope.Scoped class JavaTextEditTest { private static final File JAVA_FILE = new File("src/test/files/api/JavaFileInternalJavaIssueBuilderTest.java"); private static ClassTree classTree; diff --git a/java-frontend/src/test/java/org/sonar/java/testing/JavaFileScannerContextForTestsTest.java b/java-frontend/src/test/java/org/sonar/java/testing/JavaFileScannerContextForTestsTest.java index 40607240d27..fbb1987ec7f 100644 --- a/java-frontend/src/test/java/org/sonar/java/testing/JavaFileScannerContextForTestsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/testing/JavaFileScannerContextForTestsTest.java @@ -38,6 +38,7 @@ import org.sonar.java.reporting.AnalyzerMessage; import org.sonar.java.reporting.FluentReporting; import org.sonar.java.reporting.JavaQuickFix; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.tree.ClassTree; @@ -45,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +@TestResourceScope.Scoped class JavaFileScannerContextForTestsTest { private static final JavaCheck CHECK = new DummyRule(); diff --git a/java-frontend/src/test/java/org/sonar/java/testing/VisitorsBridgeForTestsTest.java b/java-frontend/src/test/java/org/sonar/java/testing/VisitorsBridgeForTestsTest.java index 0ed0e0a2987..29f91a36fb7 100644 --- a/java-frontend/src/test/java/org/sonar/java/testing/VisitorsBridgeForTestsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/testing/VisitorsBridgeForTestsTest.java @@ -30,6 +30,7 @@ import org.sonar.java.model.JParserTestUtils; import org.sonar.java.model.JavaVersionImpl; import org.sonar.java.reporting.AnalyzerMessage; +import org.sonar.java.test.TestResourceScope; import org.sonar.plugins.java.api.JavaFileScanner; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -38,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +@TestResourceScope.Scoped class VisitorsBridgeForTestsTest { @Test diff --git a/java-frontend/src/test/java/org/sonar/java/utils/PackageUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/utils/PackageUtilsTest.java index 29a30d158ff..70159701cb2 100644 --- a/java-frontend/src/test/java/org/sonar/java/utils/PackageUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/utils/PackageUtilsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.test.classpath.TestClasspathUtils; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.ClassTree; @@ -25,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@TestResourceScope.Scoped class PackageUtilsTest { // ---- packageName(PackageDeclarationTree, String) -------------------------- diff --git a/java-frontend/src/test/java/org/sonar/java/utils/SpringUtilsTest.java b/java-frontend/src/test/java/org/sonar/java/utils/SpringUtilsTest.java index a37248c11d1..2eee64bfb5c 100644 --- a/java-frontend/src/test/java/org/sonar/java/utils/SpringUtilsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/utils/SpringUtilsTest.java @@ -27,6 +27,7 @@ import org.sonar.java.model.declaration.VariableTreeImpl; import org.sonar.java.model.springcontext.InjectionPoint; import org.sonar.java.reporting.AnalyzerMessage; +import org.sonar.java.test.TestResourceScope; import org.sonar.java.test.classpath.TestClasspathUtils; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -38,6 +39,7 @@ import static org.sonar.java.model.springcontext.ProfileExpression.profile; import static org.assertj.core.api.Assertions.tuple; +@TestResourceScope.Scoped class SpringUtilsTest { @Test diff --git a/sonar-java-plugin/license-resources/apache.txt b/sonar-java-plugin/license-resources/apache.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/sonar-java-plugin/license-resources/apache.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/sonar-java-plugin/pom.xml b/sonar-java-plugin/pom.xml index a390364e2d4..968e2dfe732 100644 --- a/sonar-java-plugin/pom.xml +++ b/sonar-java-plugin/pom.xml @@ -289,6 +289,10 @@ true com.sonarsource|org.sonarsource.java + + https?://www\.apache\.org/licenses/LICENSE-2\.0(?:\.txt)?$ + ${project.baseUri}/license-resources/apache.txt + http://www.gnu.org/licenses/lgpl.txt ${project.baseUri}/license-resources/lgpl.txt