From 32f72bb5f13f5bd1f00407df84953916cedd2a31 Mon Sep 17 00:00:00 2001 From: Andrei Epure <38876598+andrei-epure-sonarsource@users.noreply.github.com> Date: Fri, 15 Jun 2018 15:00:48 +0200 Subject: [PATCH] SONARJAVA-2753 Rule S3305: No autowired fields in "@Configuration" class (#2067) --- java-checks/pom.xml | 5 + .../java/org/sonar/java/checks/CheckList.java | 2 + ...ConfigurationWithAutowiredFieldsCheck.java | 112 +++++++++++++++++ .../l10n/java/rules/squid/S3305_java.html | 32 +++++ .../l10n/java/rules/squid/S3305_java.json | 17 +++ .../java/rules/squid/Sonar_way_profile.json | 1 + ...ConfigurationWithAutowiredFieldsCheck.java | 118 ++++++++++++++++++ ...igurationWithAutowiredFieldsCheckTest.java | 33 +++++ 8 files changed, 320 insertions(+) create mode 100644 java-checks/src/main/java/org/sonar/java/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java create mode 100644 java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/S3305_java.html create mode 100644 java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/S3305_java.json create mode 100644 java-checks/src/test/files/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/spring/SpringConfigurationWithAutowiredFieldsCheckTest.java diff --git a/java-checks/pom.xml b/java-checks/pom.xml index dbd4dbae308..86a4f3cd3d1 100644 --- a/java-checks/pom.xml +++ b/java-checks/pom.xml @@ -227,6 +227,11 @@ 2.1 jar + + javax.inject + javax.inject + 1 + com.google.guava guava diff --git a/java-checks/src/main/java/org/sonar/java/checks/CheckList.java b/java-checks/src/main/java/org/sonar/java/checks/CheckList.java index 37f55d2e1b8..97fc1eb9721 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/CheckList.java +++ b/java-checks/src/main/java/org/sonar/java/checks/CheckList.java @@ -67,6 +67,7 @@ import org.sonar.java.checks.spring.SpringComponentWithNonAutowiredMembersCheck; import org.sonar.java.checks.spring.SpringComponentWithWrongScopeCheck; import org.sonar.java.checks.spring.SpringComposedRequestMappingCheck; +import org.sonar.java.checks.spring.SpringConfigurationWithAutowiredFieldsCheck; import org.sonar.java.checks.spring.SpringRequestMappingMethodCheck; import org.sonar.java.checks.spring.SpringScanDefaultPackageCheck; import org.sonar.java.checks.spring.SpringSecurityDebugModeCheck; @@ -530,6 +531,7 @@ public static List> getJavaChecks() { .add(StaticFieldUpdateInConstructorCheck.class) .add(NestedTernaryOperatorsCheck.class) .add(SpringComponentWithNonAutowiredMembersCheck.class) + .add(SpringConfigurationWithAutowiredFieldsCheck.class) .add(SpringComponentWithWrongScopeCheck.class) .add(SpringComposedRequestMappingCheck.class) .add(SpringRequestMappingMethodCheck.class) diff --git a/java-checks/src/main/java/org/sonar/java/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java new file mode 100644 index 00000000000..67f27682096 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java @@ -0,0 +1,112 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.java.checks.spring; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.VariableTree; + +@Rule(key = "S3305") +public class SpringConfigurationWithAutowiredFieldsCheck extends IssuableSubscriptionVisitor { + + private static final String MESSAGE_FORMAT = "Inject this field value directly into \"%s\", the only method that uses it."; + + private static final String CONFIGURATION_ANNOTATION = "org.springframework.context.annotation.Configuration"; + private static final String BEAN_ANNOTATION = "org.springframework.context.annotation.Bean"; + private static final List AUTOWIRED_ANNOTATIONS = Arrays.asList( + "org.springframework.beans.factory.annotation.Autowired", + "javax.inject.Inject"); + + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.CLASS); + } + + @Override + public void visitNode(Tree tree) { + ClassTree classTree = (ClassTree) tree; + if (classTree.symbol().metadata().isAnnotatedWith(CONFIGURATION_ANNOTATION)) { + Map autowiredFields = new HashMap<>(); + classTree.members().forEach(m -> collectAutowiredFields(m, autowiredFields)); + + Map> methodsThatUseAutowiredFields = new HashMap<>(); + autowiredFields.keySet().forEach(f -> methodsThatUseAutowiredFields.put(f, new ArrayList<>())); + classTree.members().forEach(m -> collectMethodsThatUseAutowiredFields(m, methodsThatUseAutowiredFields)); + + // report autowired fields that are used by a single method, if that method is @Bean + methodsThatUseAutowiredFields.entrySet().stream() + .filter(methodsForField -> methodsForField.getValue().size() == 1 && + methodsForField.getValue().get(0).symbol().metadata().isAnnotatedWith(BEAN_ANNOTATION)) + .forEach(methodsForField -> reportIssue( + autowiredFields.get(methodsForField.getKey()).simpleName(), + String.format(MESSAGE_FORMAT, methodsForField.getValue().get(0).simpleName().name()))); + } + } + + private static void collectAutowiredFields(Tree tree, Map autowiredFields) { + if (!tree.is(Tree.Kind.VARIABLE)) { + return; + } + VariableTree variable = (VariableTree) tree; + Symbol variableSymbol = variable.symbol(); + if (AUTOWIRED_ANNOTATIONS.stream().anyMatch(a -> variableSymbol.metadata().isAnnotatedWith(a))) { + autowiredFields.put(variableSymbol, variable); + } + } + + private static void collectMethodsThatUseAutowiredFields(Tree tree, Map> methodsThatUseAutowiredFields) { + if (!tree.is(Tree.Kind.METHOD)) { + return; + } + IdentifiersVisitor identifiersVisitor = new IdentifiersVisitor(methodsThatUseAutowiredFields.keySet()); + tree.accept(identifiersVisitor); + // for each autowired field that is referenced in this method, add the current method name to the list + identifiersVisitor.isFieldReferenced.entrySet().stream() + .filter(Map.Entry::getValue) + .map(Map.Entry::getKey) + .forEach(field -> methodsThatUseAutowiredFields.get(field).add((MethodTree) tree)); + } + + private static class IdentifiersVisitor extends BaseTreeVisitor { + private final Map isFieldReferenced = new HashMap<>(); + + IdentifiersVisitor(Set autowiredFields) { + autowiredFields.forEach(f -> isFieldReferenced.put(f, false)); + } + + @Override + public void visitIdentifier(IdentifierTree identifierTree) { + isFieldReferenced.computeIfPresent(identifierTree.symbol(), (fieldSym, isPresent) -> true); + } + } +} diff --git a/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/S3305_java.html b/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/S3305_java.html new file mode 100644 index 00000000000..ea8cae67c93 --- /dev/null +++ b/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/S3305_java.html @@ -0,0 +1,32 @@ +

When @Autowired is used, dependencies need to be resolved when the class is instantiated, which may cause early initialization of +beans or lead the context to look in places it shouldn't to find the bean. To avoid this tricky issue and optimize the way the context loads, +dependencies should be requested as late as possible. That means using parameter injection instead of field injection for dependencies that are only +used in a single @Bean method.

+

Noncompliant Code Example

+
+@Configuration
+public class ​FooConfiguration {
+
+  @Autowired private ​DataSource dataSource​;  // Noncompliant
+
+  @Bean
+  public ​MyService myService() {
+    return new ​MyService(this​.dataSource​);
+  }
+}
+
+

Compliant Solution

+
+@Configuration
+public class ​FooConfiguration {
+
+ @Bean
+  public ​MyService myService(DataSource dataSource) {
+    return new ​MyService(dataSource);
+  }
+}
+
+

Exceptions

+

Fields used in methods that are called directly by other methods in the application (as opposed to being invoked automatically by the Spring +framework) are ignored by this rule so that direct callers don't have to provide the dependencies themselves.

+ diff --git a/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/S3305_java.json b/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/S3305_java.json new file mode 100644 index 00000000000..77e1f995871 --- /dev/null +++ b/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/S3305_java.json @@ -0,0 +1,17 @@ +{ + "title": "Factory method injection should be used in \"@Configuration\" classes", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "spring", + "performance" + ], + "defaultSeverity": "Critical", + "ruleSpecification": "RSPEC-3305", + "sqKey": "S3305", + "scope": "Main" +} diff --git a/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/Sonar_way_profile.json b/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/Sonar_way_profile.json index 9a955976bf5..53da947249e 100644 --- a/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/Sonar_way_profile.json +++ b/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/Sonar_way_profile.json @@ -257,6 +257,7 @@ "S3066", "S3067", "S3281", + "S3305", "S3346", "S3355", "S3358", diff --git a/java-checks/src/test/files/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java b/java-checks/src/test/files/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java new file mode 100644 index 00000000000..abbc9198a2d --- /dev/null +++ b/java-checks/src/test/files/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java @@ -0,0 +1,118 @@ +package src.test.files.checks.spring; + +import javax.inject.Inject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +class Bar { } + +class Foo { + private final Bar bar; + public Foo(Bar bar) { this.bar = bar; } +} + +@Configuration +class A { + + @Autowired private Bar singleUsage; // Noncompliant [[sc=26;ec=37]] {{Inject this field value directly into "method", the only method that uses it.}} + @Inject private Bar jsr330; // Noncompliant [[sc=23;ec=29]] {{Inject this field value directly into "jsr330", the only method that uses it.}} + @Autowired private Bar multipleUsage; + @Autowired private Bar notUsedInBeanMethod; + @Autowired private Bar notUsed; + private Bar notAutowired; + + @Bean + public Foo method() { + return new Foo(this.singleUsage); + } + + @Bean + public Foo jsr330() { + return new Foo(this.jsr330); + } + + @Bean + public Foo method2() { + return new Foo(this.multipleUsage); + } + + @Bean + public Foo method3() { + return new Foo(this.multipleUsage); + } + + public Foo method4() { + return new Foo(this.notUsedInBeanMethod); + } + + @Bean + public Foo method5() { + return new Foo(this.notAutowired); + } +} + +@Configuration +class B { + @Autowired private Bar multipleUsage; + @Bean + public Foo method() { + return indirectMethod(); + } + private Foo indirectMethod() { + return new Foo(this.multipleUsage); + } + @Bean Foo method2() { + return new Foo(this.multipleUsage); + } +} + +@Configuration +class FalseNegative { + + private Bar bar; // FN + + @Autowired + public void setBar(Bar bar) { + this.bar = bar; + } + + @Bean + public Foo method() { + return new Foo(this.bar); + } +} + +@Configuration +class Ok { + + @Bean + public Foo method(Bar bar) { + return new Foo(bar); + } +} + +@Configuration +public class NestedConfig { + + @Configuration + public class InnerConfig { + @Autowired private Bar x; // Noncompliant + @Bean + public Foo method() { + return new Foo(this.x); + } + } +} + +public class StaticConfig { + @Configuration + public static class InnerStaticConfig { + @Autowired private Bar x; // Noncompliant + + @Bean + public Foo method() { + return new Foo(this.x); + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/spring/SpringConfigurationWithAutowiredFieldsCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/spring/SpringConfigurationWithAutowiredFieldsCheckTest.java new file mode 100644 index 00000000000..40d290cec71 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/spring/SpringConfigurationWithAutowiredFieldsCheckTest.java @@ -0,0 +1,33 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.java.checks.spring; + +import org.junit.Test; +import org.sonar.java.checks.verifier.JavaCheckVerifier; + +public class SpringConfigurationWithAutowiredFieldsCheckTest { + + @Test + public void test() { + JavaCheckVerifier.verify("src/test/files/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java", new SpringConfigurationWithAutowiredFieldsCheck()); + JavaCheckVerifier.verifyNoIssueWithoutSemantic("src/test/files/checks/spring/SpringConfigurationWithAutowiredFieldsCheck.java", new SpringConfigurationWithAutowiredFieldsCheck()); + } + +}