diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8989.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8989.json new file mode 100644 index 00000000000..4019d11648a --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8989.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8989", + "hasTruePositives": false, + "falseNegatives": 12, + "falsePositives": 0 +} diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/TransactionalMethodCheckedExceptionCheckSampleNonCompiling.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/TransactionalMethodCheckedExceptionCheckSampleNonCompiling.java new file mode 100644 index 00000000000..114af2f93ea --- /dev/null +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/TransactionalMethodCheckedExceptionCheckSampleNonCompiling.java @@ -0,0 +1,15 @@ +package checks.spring; + +import org.springframework.transaction.annotation.Transactional; + +class TransactionalMethodCheckedExceptionCheckSampleNonCompiling { + + // Test unknown/unresolved exception type + @Transactional + public void unknownException() throws UnresolvedCheckedException { // No issue - type is unknown + } + + @Transactional // Noncompliant + public void knownException() throws java.io.IOException { + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java new file mode 100644 index 00000000000..27b7c44d3f5 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodCheckedExceptionCheckSample.java @@ -0,0 +1,172 @@ +package checks.spring; + +import java.io.IOException; +import java.sql.SQLException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.transaction.annotation.Transactional; + +// Composed/meta-annotation for testing +@Retention(RetentionPolicy.RUNTIME) +@Transactional +@interface MyTransactional { +} + +class Order {} +class NotificationException extends Exception {} +class CustomCheckedException extends Exception {} + +public class TransactionalMethodCheckedExceptionCheckSample { + + @Transactional // Noncompliant {{Specify rollback behavior for checked exceptions using "rollbackFor" or "noRollbackFor" attributes.}} [[quickfixes=qf1,qf2]] +//^^^^^^^^^^^^^^ + // fix@qf1 {{Add rollbackFor attribute}} + // edit@qf1 [[sc=3;ec=17]] {{@Transactional(rollbackFor = {java.io.IOException.class, java.sql.SQLException.class})}} + // fix@qf2 {{Add rollbackFor = Exception.class}} + // edit@qf2 [[sc=3;ec=17]] {{@Transactional(rollbackFor = java.lang.Exception.class)}} + public void processOrder(Order order) throws IOException, SQLException { + } + + @Transactional // Noncompliant [[quickfixes=qf3]] +//^^^^^^^^^^^^^^ + // fix@qf3 {{Add rollbackFor = Exception.class}} + // edit@qf3 [[sc=3;ec=17]] {{@Transactional(rollbackFor = java.lang.Exception.class)}} + public void importData() throws Exception { + } + + @Transactional(timeout = 30) // Noncompliant +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + public void withOtherAttributes() throws SQLException { + } + + @Transactional // Noncompliant [[quickfixes=qf7,qf8]] +//^^^^^^^^^^^^^^ + // fix@qf7 {{Add rollbackFor attribute}} + // edit@qf7 [[sc=3;ec=17]] {{@Transactional(rollbackFor = checks.spring.CustomCheckedException.class)}} + // fix@qf8 {{Add rollbackFor = Exception.class}} + // edit@qf8 [[sc=3;ec=17]] {{@Transactional(rollbackFor = java.lang.Exception.class)}} + public void customException() throws CustomCheckedException { + } + + @Transactional // Noncompliant [[quickfixes=qf9,qf10]] +//^^^^^^^^^^^^^^ + // fix@qf9 {{Add rollbackFor attribute}} + // edit@qf9 [[sc=3;ec=17]] {{@Transactional(rollbackFor = java.io.IOException.class)}} + // fix@qf10 {{Add rollbackFor = Exception.class}} + // edit@qf10 [[sc=3;ec=17]] {{@Transactional(rollbackFor = java.lang.Exception.class)}} + public void mixedExceptions() throws IOException, RuntimeException { + } + + @Transactional(rollbackFor = IOException.class) + public void withRollbackFor() throws IOException { + } + + @Transactional(rollbackFor = {IOException.class, SQLException.class}) + public void withMultipleRollbackFor() throws IOException, SQLException { + } + + @Transactional(rollbackFor = Exception.class) + public void rollbackForAll() throws Exception { + } + + @Transactional(noRollbackFor = NotificationException.class) + public void withNoRollbackFor() throws NotificationException { + } + + @Transactional(rollbackFor = Exception.class, noRollbackFor = NotificationException.class) + public void withBoth() throws Exception { + } + + @Transactional(rollbackForClassName = "java.io.IOException") + public void rollbackForClassName() throws IOException { + } + + @Transactional(noRollbackForClassName = "checks.spring.NotificationException") + public void noRollbackForClassName() throws NotificationException { + } + + @Transactional + public void noExceptions() { + } + + @Transactional + public void uncheckedOnly() throws RuntimeException { + } + + public void noAnnotation() throws IOException { + } + + @Transactional(rollbackFor = Exception.class) + static class ClassLevelConfig { + public void inherited() throws IOException { + } + } + + @Transactional // Noncompliant [[quickfixes=qf11,qf12]] +//^^^^^^^^^^^^^^ + // fix@qf11 {{Add rollbackFor attribute}} + // edit@qf11 [[sc=3;ec=17]] {{@Transactional(rollbackFor = java.io.IOException.class)}} + // fix@qf12 {{Add rollbackFor = Exception.class}} + // edit@qf12 [[sc=3;ec=17]] {{@Transactional(rollbackFor = java.lang.Exception.class)}} + static class ClassLevelNoConfig { + public void noConfig() throws IOException { + } + + @Transactional(rollbackFor = IOException.class) + public void methodOverrides() throws IOException { + } + } + + @Transactional + public void errorNotChecked() throws Error { + } + + @org.springframework.transaction.annotation.Transactional // Noncompliant [[quickfixes=qf13,qf14]] +//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>^^^^^^^^^^^^^^^^^^^^^^^^ + // fix@qf13 {{Add rollbackFor attribute}} + // edit@qf13 [[sc=3;ec=60]] {{@org.springframework.transaction.annotation.Transactional(rollbackFor = java.io.IOException.class)}} + // fix@qf14 {{Add rollbackFor = Exception.class}} + // edit@qf14 [[sc=3;ec=60]] {{@org.springframework.transaction.annotation.Transactional(rollbackFor = java.lang.Exception.class)}} + public void fullyQualified() throws IOException { + } + + @Transactional(rollbackFor = IOException.class) + public void partialConfig() throws SQLException { + } + + @Transactional(value = "txManager") // Noncompliant +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + public void withValueAttribute() throws IOException { + } + + // Test nested structure to ensure parent traversal works + static class OuterClass { + @Transactional // Noncompliant + static class InnerClassWithAnnotation { + public void nestedMethod() throws IOException { + } + } + + static class InnerClassNoAnnotation { + public void nestedMethodNoAnnotation() throws IOException { + // No @Transactional at any level, so no issue + } + } + } + + @Transactional // Noncompliant + interface TransactionalInterface { + void interfaceMethod() throws IOException; + } + + // Test meta-annotated (composed) annotation + @MyTransactional // Noncompliant + public void metaAnnotated() throws IOException { + } + + // Test annotation with value attribute (transaction manager name) + @Transactional("txManager") // Noncompliant + public void valueShorthand() throws IOException { + // Has value attribute but no rollback configuration + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheck.java b/java-checks/src/main/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheck.java new file mode 100644 index 00000000000..2326574ffb8 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheck.java @@ -0,0 +1,197 @@ +/* + * 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.checks.spring; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.QuickFixHelper; +import org.sonar.java.checks.helpers.SpringUtils; +import org.sonar.java.reporting.JavaQuickFix; +import org.sonar.java.reporting.JavaTextEdit; +import org.sonar.plugins.java.api.DependencyVersionAware; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.Version; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.AnnotationTree; +import org.sonar.plugins.java.api.tree.Arguments; +import org.sonar.plugins.java.api.tree.ClassTree; +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.TypeTree; + +@Rule(key = "S8989") +public class TransactionalMethodCheckedExceptionCheck extends IssuableSubscriptionVisitor implements DependencyVersionAware { + + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.METHOD); + } + + @Override + public void visitNode(Tree tree) { + MethodTree method = (MethodTree) tree; + + List throwsClauses = method.throwsClauses(); + if (throwsClauses.isEmpty()) { + return; + } + + List checkedExceptions = throwsClauses.stream() + .map(TypeTree::symbolType) + .filter(TransactionalMethodCheckedExceptionCheck::isCheckedException) + .toList(); + + if (checkedExceptions.isEmpty()) { + return; + } + + AnnotationTree transactionalAnnotation = getTransactionalAnnotation(method); + if (transactionalAnnotation == null) { + return; + } + + // Check if the annotation tree itself has rollback configuration + if (hasRollbackConfiguration(transactionalAnnotation)) { + return; + } + + QuickFixHelper.newIssue(context) + .forRule(this) + .onTree(transactionalAnnotation) + .withMessage("Specify rollback behavior for checked exceptions using \"rollbackFor\" or \"noRollbackFor\" attributes.") + .withQuickFixes(() -> computeQuickFixes(transactionalAnnotation, checkedExceptions)) + .report(); + } + + private static AnnotationTree getTransactionalAnnotation(MethodTree method) { + // Check method-level annotation first (including meta-annotations) + for (AnnotationTree annotation : method.modifiers().annotations()) { + if (isTransactionalAnnotation(annotation)) { + return annotation; + } + } + + // Check class-level annotation + Tree parent = method.parent(); + while (parent != null && !parent.is(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.RECORD)) { + parent = parent.parent(); + } + + if (parent instanceof ClassTree classTree) { + for (AnnotationTree annotation : classTree.modifiers().annotations()) { + if (isTransactionalAnnotation(annotation)) { + return annotation; + } + } + } + + return null; + } + + private static boolean isTransactionalAnnotation(AnnotationTree annotation) { + // Check if the annotation itself is @Transactional + if (annotation.symbolType().is(SpringUtils.TRANSACTIONAL_ANNOTATION)) { + return true; + } + // Check if the annotation is meta-annotated with @Transactional (composed annotation) + return annotation.symbolType().symbol().metadata().isAnnotatedWith(SpringUtils.TRANSACTIONAL_ANNOTATION); + } + + private List computeQuickFixes(AnnotationTree annotation, List checkedExceptions) { + List quickFixes = new ArrayList<>(); + + // Quick fix 1: Add rollbackFor with all checked exceptions (using fully qualified names) + String exceptionsList = checkedExceptions.stream() + .map(Type::fullyQualifiedName) + .map(name -> name + ".class") + .collect(Collectors.joining(", ")); + + String rollbackForAttribute = (checkedExceptions.size() == 1) + ? ("rollbackFor = " + exceptionsList) + : ("rollbackFor = {" + exceptionsList + "}"); + + // Only add the specific exceptions quick fix if it's different from Exception.class + boolean isAlreadyExceptionClass = checkedExceptions.size() == 1 + && "java.lang.Exception".equals(checkedExceptions.get(0).fullyQualifiedName()); + + if (!isAlreadyExceptionClass) { + quickFixes.add(createQuickFix(annotation, rollbackForAttribute, "Add rollbackFor attribute")); + } + + // Quick fix 2: Add rollbackFor = Exception.class (covers all checked exceptions) + quickFixes.add(createQuickFix(annotation, "rollbackFor = java.lang.Exception.class", "Add rollbackFor = Exception.class")); + + return quickFixes; + } + + private JavaQuickFix createQuickFix(AnnotationTree annotation, String attribute, String description) { + Arguments arguments = annotation.arguments(); + + if (arguments.isEmpty()) { + // @Transactional -> @Transactional(rollbackFor = ...) + String annotationTypeName = QuickFixHelper.contentForTree(annotation.annotationType(), context); + String replacement = annotationTypeName + "(" + attribute + ")"; + return JavaQuickFix.newQuickFix(description) + .addTextEdit(JavaTextEdit.replaceTree(annotation, "@" + replacement)) + .build(); + } else { + // @Transactional(timeout = 30) -> @Transactional(timeout = 30, rollbackFor = ...) + SyntaxToken closeParenToken = arguments.closeParenToken(); + return JavaQuickFix.newQuickFix(description) + .addTextEdit(JavaTextEdit.insertBeforeTree(closeParenToken, ", " + attribute)) + .build(); + } + } + + private static boolean isCheckedException(Type type) { + if (type.isUnknown()) { + return false; + } + + return type.isSubtypeOf("java.lang.Exception") + && !type.isSubtypeOf("java.lang.RuntimeException") + && !type.isSubtypeOf("java.lang.Error"); + } + + private static boolean hasRollbackConfiguration(AnnotationTree annotation) { + return annotation.arguments().stream() + .anyMatch(arg -> { + if (arg.is(Tree.Kind.ASSIGNMENT)) { + var assignment = (org.sonar.plugins.java.api.tree.AssignmentExpressionTree) arg; + String name = ((org.sonar.plugins.java.api.tree.IdentifierTree) assignment.variable()).name(); + return "rollbackFor".equals(name) + || "rollbackForClassName".equals(name) + || "noRollbackFor".equals(name) + || "noRollbackForClassName".equals(name); + } + return false; + }); + } + + @Override + public boolean isCompatibleWithDependencies(Function> dependencyFinder) { + Optional springContextVersion = dependencyFinder.apply("spring-context"); + Optional springTxVersion = dependencyFinder.apply("spring-tx"); + return springTxVersion.isPresent() || springContextVersion.isPresent(); + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheckTest.java new file mode 100644 index 00000000000..e574d420478 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/spring/TransactionalMethodCheckedExceptionCheckTest.java @@ -0,0 +1,52 @@ +/* + * 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.checks.spring; + +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; +import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; + +class TransactionalMethodCheckedExceptionCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/spring/TransactionalMethodCheckedExceptionCheckSample.java")) + .withCheck(new TransactionalMethodCheckedExceptionCheck()) + .verifyIssues(); + } + + @Test + void test_noDependencies() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/spring/TransactionalMethodCheckedExceptionCheckSample.java")) + .withCheck(new TransactionalMethodCheckedExceptionCheck()) + .withClassPath(Collections.emptyList()) + .verifyNoIssues(); + } + + @Test + void test_nonCompiling() { + CheckVerifier.newVerifier() + .onFile(nonCompilingTestSourcesPath("checks/spring/TransactionalMethodCheckedExceptionCheckSampleNonCompiling.java")) + .withCheck(new TransactionalMethodCheckedExceptionCheck()) + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8989.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8989.html new file mode 100644 index 00000000000..a69c9b41f48 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8989.html @@ -0,0 +1,116 @@ +

This is an issue when a method is annotated with @Transactional and declares one or more checked exceptions in its throws +clause, but does not explicitly configure how the transaction should behave when those exceptions occur.

+

In Spring Framework, the @Transactional annotation controls database transactions. By default:

+
    +
  • Unchecked exceptions (subclasses of RuntimeException or Error) automatically trigger a transaction + rollback
  • +
  • Checked exceptions do NOT trigger a rollback - the transaction commits even when these exceptions are thrown
  • +
+

When you declare a checked exception in your method signature but don’t specify the rollback behavior, Spring will commit the transaction even if +that exception occurs. This often leads to data being saved in an inconsistent state.

+

Why is this an issue?

+

When a method throws a checked exception, it typically signals that something went wrong during execution. In the context of database transactions, +you usually want to roll back the transaction when errors occur to maintain data integrity.

+

However, Spring’s @Transactional annotation only rolls back transactions automatically for unchecked exceptions (like +RuntimeException). For checked exceptions, the transaction commits by default - even when the exception indicates a failure.

+

This default behavior creates a dangerous mismatch between intent and outcome:

+
    +
  • Your code throws an exception to signal an error
  • +
  • The calling code catches the exception and treats it as a failure
  • +
  • But the database transaction has already committed, saving partial or incorrect data
  • +
+

This silent failure mode is particularly problematic because:

+
    +
  • It’s not obvious from the code that checked exceptions won’t trigger rollback
  • +
  • The behavior differs from what most developers expect
  • +
  • Partial transaction commits can leave your database in an inconsistent state
  • +
+

By explicitly specifying rollbackFor or noRollbackFor, you make the intended behavior clear and prevent unexpected +transaction commits.

+

What is the potential impact?

+

When checked exceptions don’t trigger rollbacks as expected, the consequences can be severe:

+

Data Corruption

+

Partial transactions commit to the database, leaving data in an inconsistent state. For example, if an order processing method saves an order but +fails when creating the invoice, you end up with an order record but no corresponding invoice.

+

Silent Failures

+

The application throws an exception indicating failure, but the database changes persist anyway. Error handling code may log the error or show a +failure message to users, while the "failed" operation actually modified the database.

+

Business Logic Violations

+

Multi-step operations that should be atomic (all-or-nothing) can partially complete. This violates business rules that depend on data consistency, +such as:

+
    +
  • Account balances not matching transaction history
  • +
  • Inventory counts becoming incorrect
  • +
  • Orphaned records without their required relationships
  • +
+

Difficult Debugging

+

Because the transaction commits silently, these bugs are hard to diagnose. The code appears to handle errors correctly (it throws and catches +exceptions), but the database state doesn’t match expectations.

+

How to fix it in Spring

+

Explicitly specify rollbackFor to include the checked exceptions that should trigger a rollback. This is the most common fix, as most +checked exceptions represent error conditions that should prevent the transaction from committing.

+

Code examples

+

Noncompliant code example

+
+@Transactional
+public void processOrder(Order order) throws IOException, SQLException { // Noncompliant
+    orderRepository.save(order);
+    notificationService.sendConfirmation(order); // May throw IOException
+}
+
+

Compliant solution

+
+@Transactional(rollbackFor = {IOException.class, SQLException.class})
+public void processOrder(Order order) throws IOException, SQLException {
+    orderRepository.save(order);
+    notificationService.sendConfirmation(order); // May throw IOException
+}
+
+

If you want to roll back on all exceptions (both checked and unchecked), you can specify Exception.class as the rollback trigger. This +is a safe default when you’re unsure which specific exceptions to list.

+

Noncompliant code example

+
+@Transactional
+public void importData(File file) throws Exception { // Noncompliant
+    List<Record> records = parser.parse(file);
+    recordRepository.saveAll(records);
+}
+
+

Compliant solution

+
+@Transactional(rollbackFor = Exception.class)
+public void importData(File file) throws Exception {
+    List<Record> records = parser.parse(file);
+    recordRepository.saveAll(records);
+}
+
+

In rare cases where you intentionally want the transaction to commit even when a checked exception is thrown, use noRollbackFor to +document this decision explicitly. This makes it clear the behavior is intentional, not an oversight.

+

Noncompliant code example

+
+@Transactional
+public void processWithNotification(Order order) throws NotificationException { // Noncompliant
+    orderRepository.save(order);
+    // We want order saved even if notification fails
+    notificationService.sendEmail(order);
+}
+
+

Compliant solution

+
+@Transactional(noRollbackFor = NotificationException.class)
+public void processWithNotification(Order order) throws NotificationException {
+    orderRepository.save(order);
+    // We want order saved even if notification fails
+    notificationService.sendEmail(order);
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8989.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8989.json new file mode 100644 index 00000000000..30a5819853d --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8989.json @@ -0,0 +1,26 @@ +{ + "title": "Methods with \"@Transactional\" should specify rollback behavior for checked exceptions", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5 min" + }, + "tags": [ + "spring", + "transaction", + "pitfall" + ], + "defaultSeverity": "Critical", + "ruleSpecification": "RSPEC-8989", + "sqKey": "S8989", + "scope": "Main", + "quickfix": "unknown", + "code": { + "impacts": { + "RELIABILITY": "MEDIUM", + "MAINTAINABILITY": "HIGH" + }, + "attribute": "COMPLETE" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S8989 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S8989 new file mode 100644 index 00000000000..e69de29bb2d