From 3d80c6c894d7cb2ba4cb63180fbeb92acb8e2ce4 Mon Sep 17 00:00:00 2001 From: Alban Auzeill Date: Fri, 22 Sep 2023 09:50:30 +0200 Subject: [PATCH] SONARJAVA-4609 [Custom Rules] CheckRegistrar classes can register check instances, default quality profile and AutoScan (#4470) --- .../java/MyJavaFileCheckRegistrarTest.java | 35 +++- .../samples/java/MyJavaRulesPluginTest.java | 7 +- .../verifier/TestCheckRegistrarContext.java | 104 ++++++++++ .../verifier/TestProfileRegistrarContext.java | 37 ++++ .../internal/InternalCheckVerifier.java | 2 +- .../TestCheckRegistrarContextTest.java | 131 ++++++++++++ .../TestProfileRegistrarContextTest.java | 38 ++++ .../org/sonar/java/checks/SanityTest.java | 2 +- .../sonar/java/filters/FilterVerifier.java | 2 +- .../java/org/sonar/java/SonarComponents.java | 91 ++++++--- .../plugins/java/api/CheckRegistrar.java | 85 +++++++- .../plugins/java/api/ProfileRegistrar.java | 65 ++++++ .../java/org/sonar/java/JavaFrontendTest.java | 4 +- .../org/sonar/java/SonarComponentsTest.java | 189 ++++++++++++++---- .../sonar/java/ast/JavaAstScannerTest.java | 4 +- .../visitors/SonarSymbolTableVisitorTest.java | 3 +- .../SyntaxHighlighterVisitorTest.java | 3 +- ...leScannerContextWithSensorContextTest.java | 2 +- .../DefaultModuleScannerContextTest.java | 6 +- .../sonar/java/model/VisitorsBridgeTest.java | 4 +- .../JavaFileScannerContextForTestsTest.java | 2 +- .../testing/VisitorsBridgeForTestsTest.java | 6 +- .../plugins/java/api/CheckRegistrarTest.java | 113 +++++++++-- .../java/se/JavaFrontendIntegrationTest.java | 4 +- .../org/sonar/plugins/java/JavaSensor.java | 31 +-- .../plugins/java/JavaSonarWayProfile.java | 35 +++- .../main/resources/static/documentation.md | 15 ++ .../sonar/plugins/java/JavaSensorTest.java | 19 +- .../plugins/java/JavaSonarWayProfileTest.java | 32 ++- 29 files changed, 912 insertions(+), 159 deletions(-) create mode 100644 java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/TestCheckRegistrarContext.java create mode 100644 java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/TestProfileRegistrarContext.java create mode 100644 java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/TestCheckRegistrarContextTest.java create mode 100644 java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/TestProfileRegistrarContextTest.java create mode 100644 java-frontend/src/main/java/org/sonar/plugins/java/api/ProfileRegistrar.java diff --git a/docs/java-custom-rules-example/src/test/java/org/sonar/samples/java/MyJavaFileCheckRegistrarTest.java b/docs/java-custom-rules-example/src/test/java/org/sonar/samples/java/MyJavaFileCheckRegistrarTest.java index 3c6683290bc..c98b65be502 100644 --- a/docs/java-custom-rules-example/src/test/java/org/sonar/samples/java/MyJavaFileCheckRegistrarTest.java +++ b/docs/java-custom-rules-example/src/test/java/org/sonar/samples/java/MyJavaFileCheckRegistrarTest.java @@ -5,20 +5,45 @@ package org.sonar.samples.java; import org.junit.jupiter.api.Test; -import org.sonar.plugins.java.api.CheckRegistrar; +import org.sonar.api.rule.RuleKey; +import org.sonar.java.checks.verifier.TestCheckRegistrarContext; + import static org.assertj.core.api.Assertions.assertThat; class MyJavaFileCheckRegistrarTest { @Test - void checkNumberRules() { - CheckRegistrar.RegistrarContext context = new CheckRegistrar.RegistrarContext(); + void checkRegisteredRulesKeysAndClasses() { + TestCheckRegistrarContext context = new TestCheckRegistrarContext(); MyJavaFileCheckRegistrar registrar = new MyJavaFileCheckRegistrar(); registrar.register(context); - assertThat(context.checkClasses()).hasSize(8); - assertThat(context.testCheckClasses()).hasSize(1); + assertThat(context.mainRuleKeys).extracting(RuleKey::toString).containsExactly( + "mycompany-java:SpringControllerRequestMappingEntity", + "mycompany-java:AvoidAnnotation", + "mycompany-java:AvoidBrandInMethodNames", + "mycompany-java:AvoidMethodDeclaration", + "mycompany-java:AvoidSuperClass", + "mycompany-java:AvoidTreeList", + "mycompany-java:AvoidMethodWithSameTypeInArgument", + "mycompany-java:SecurityAnnotationMandatory"); + + assertThat(context.mainCheckClasses).extracting(Class::getSimpleName).containsExactly( + "SpringControllerRequestMappingEntityRule", + "AvoidAnnotationRule", + "AvoidBrandInMethodNamesRule", + "AvoidMethodDeclarationRule", + "AvoidSuperClassRule", + "AvoidTreeListRule", + "MyCustomSubscriptionRule", + "SecurityAnnotationMandatoryRule"); + + assertThat(context.testRuleKeys).extracting(RuleKey::toString).containsExactly( + "mycompany-java:NoIfStatementInTests"); + + assertThat(context.testCheckClasses).extracting(Class::getSimpleName).containsExactly( + "NoIfStatementInTestsRule"); } } diff --git a/docs/java-custom-rules-example/src/test/java/org/sonar/samples/java/MyJavaRulesPluginTest.java b/docs/java-custom-rules-example/src/test/java/org/sonar/samples/java/MyJavaRulesPluginTest.java index c9cf5e084e0..50da93d0d4e 100644 --- a/docs/java-custom-rules-example/src/test/java/org/sonar/samples/java/MyJavaRulesPluginTest.java +++ b/docs/java-custom-rules-example/src/test/java/org/sonar/samples/java/MyJavaRulesPluginTest.java @@ -22,8 +22,13 @@ void testName() { new MyJavaRulesPlugin().define(context); - assertThat(context.getExtensions()).hasSize(2); + assertThat(context.getExtensions()) + .extracting(ext -> ((Class) ext).getSimpleName()) + .containsExactlyInAnyOrder( + "MyJavaRulesDefinition", + "MyJavaFileCheckRegistrar"); } + public static class MockedSonarRuntime implements SonarRuntime { @Override diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/TestCheckRegistrarContext.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/TestCheckRegistrarContext.java new file mode 100644 index 00000000000..ab5b04dfede --- /dev/null +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/TestCheckRegistrarContext.java @@ -0,0 +1,104 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2023 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.verifier; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleAnnotationUtils; +import org.sonar.plugins.java.api.CheckRegistrar; +import org.sonar.plugins.java.api.JavaCheck; + +public class TestCheckRegistrarContext extends CheckRegistrar.RegistrarContext { + + public final List> mainCheckClasses = new ArrayList<>(); + public final List mainCheckInstances = new ArrayList<>(); + public final List mainRuleKeys = new ArrayList<>(); + + public final List> testCheckClasses = new ArrayList<>(); + public final List testCheckInstances = new ArrayList<>(); + public final List testRuleKeys = new ArrayList<>(); + + public final Set autoScanCompatibleRules = new HashSet<>(); + + @Override + public void registerMainChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { + validateAndRegisterChecks(repositoryKey, javaCheckClassesAndInstances, mainCheckClasses, mainCheckInstances, mainRuleKeys); + } + + @Override + public void registerTestChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { + validateAndRegisterChecks(repositoryKey, javaCheckClassesAndInstances, testCheckClasses, testCheckInstances, testRuleKeys); + } + + @Override + public void registerMainSharedCheck(JavaCheck check, Collection ruleKeys) { + mainCheckClasses.add(check.getClass()); + mainCheckInstances.add(check); + mainRuleKeys.addAll(ruleKeys); + } + + @Override + public void registerTestSharedCheck(JavaCheck check, Collection ruleKeys) { + testCheckClasses.add(check.getClass()); + testCheckInstances.add(check); + testRuleKeys.addAll(ruleKeys); + } + + @Override + public void registerAutoScanCompatibleRules(Collection ruleKeys) { + autoScanCompatibleRules.addAll(ruleKeys); + } + + private static void validateAndRegisterChecks(String repositoryKey, + Collection javaCheckClassesAndInstances, + List> destCheckClasses, + List destCheckInstances, + List destRuleKeys) { + if (StringUtils.isBlank(repositoryKey)) { + throw new IllegalArgumentException("Please specify a non blank repository key"); + } + for (Object javaCheckClassOrInstance : javaCheckClassesAndInstances) { + Class checkClass; + JavaCheck check; + try { + if (javaCheckClassOrInstance instanceof Class) { + checkClass = (Class) javaCheckClassOrInstance; + check = checkClass.getDeclaredConstructor().newInstance(); + } else { + check = (JavaCheck) javaCheckClassOrInstance; + checkClass = check.getClass(); + } + } catch (ClassCastException | NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) { + throw new IllegalStateException(String.format("Fail to instantiate %s", javaCheckClassOrInstance), e); + } + RuleKey ruleKey = RuleKey.of(repositoryKey, RuleAnnotationUtils.getRuleKey(checkClass)); + destCheckClasses.add(checkClass); + destCheckInstances.add(check); + destRuleKeys.add(ruleKey); + } + } +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/TestProfileRegistrarContext.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/TestProfileRegistrarContext.java new file mode 100644 index 00000000000..943c75c9240 --- /dev/null +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/TestProfileRegistrarContext.java @@ -0,0 +1,37 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2023 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.verifier; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import org.sonar.api.rule.RuleKey; +import org.sonar.plugins.java.api.ProfileRegistrar; + +public class TestProfileRegistrarContext implements ProfileRegistrar.RegistrarContext { + + public final Set defaultQualityProfileRules = new HashSet<>(); + + @Override + public void registerDefaultQualityProfileRules(Collection ruleKeys) { + defaultQualityProfileRules.addAll(ruleKeys); + } + +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifier.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifier.java index e25917ddcc8..3be83cb9496 100644 --- a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifier.java +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifier.java @@ -658,7 +658,7 @@ private SonarComponents sonarComponents() { ClasspathForMain classpathForMain = new ClasspathForMain(config, fileSystem); ClasspathForTest classpathForTest = new ClasspathForTest(config, fileSystem); - SonarComponents sonarComponents = new SonarComponents(null, fileSystem, classpathForMain, classpathForTest, null) { + SonarComponents sonarComponents = new SonarComponents(null, fileSystem, classpathForMain, classpathForTest, null, null) { @Override public boolean reportAnalysisError(RecognitionException re, InputFile inputFile) { throw new AssertionError(String.format("Should not fail analysis (%s)", re.getMessage())); diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/TestCheckRegistrarContextTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/TestCheckRegistrarContextTest.java new file mode 100644 index 00000000000..a5ae383d08f --- /dev/null +++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/TestCheckRegistrarContextTest.java @@ -0,0 +1,131 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2023 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.verifier; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.sonar.api.rule.RuleKey; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.JavaCheck; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TestCheckRegistrarContextTest { + + @Rule(key = "X1") + static class Rule1 implements JavaCheck { + } + @Rule(key = "X2") + static class Rule2 implements JavaCheck { + } + @Rule(key = "X3") + static class Rule3 implements JavaCheck { + } + @Rule(key = "X4") + static class Rule4 implements JavaCheck { + } + @Rule(key = "X5") + static class Rule5 implements JavaCheck { + } + @Rule(key = "X6") + static class Rule6 implements JavaCheck { + } + static class Scanner implements JavaCheck { + } + + @Test + void store_registration() { + TestCheckRegistrarContext context = new TestCheckRegistrarContext(); + + var rule4 = new Rule4(); + + context.registerClassesForRepository( + "customRepo", + List.of(Rule1.class, Rule2.class), + List.of(Rule1.class, Rule3.class)); + + context.registerMainChecks("customRepo", List.of( + rule4, + Rule5.class)); + context.registerTestChecks("customRepo", List.of( + rule4, + Rule6.class)); + context.registerMainSharedCheck(new Scanner(), List.of( + RuleKey.of("securityRepo", "R1"), + RuleKey.of("securityRepo", "R2"))); + context.registerTestSharedCheck(new Scanner(), List.of( + RuleKey.of("securityRepo", "R3"), + RuleKey.of("securityRepo", "R4"))); + context.registerAutoScanCompatibleRules(List.of( + RuleKey.of("customRepo", "X1"))); + + assertThat(context.mainRuleKeys).extracting(RuleKey::toString).containsExactly( + "customRepo:X1", + "customRepo:X2", + "customRepo:X4", + "customRepo:X5", + "securityRepo:R1", + "securityRepo:R2"); + + assertThat(context.mainCheckClasses).extracting(Class::getSimpleName).containsExactly( + "Rule1", + "Rule2", + "Rule4", + "Rule5", + "Scanner"); + + assertThat(context.testRuleKeys).extracting(RuleKey::toString).containsExactly( + "customRepo:X1", + "customRepo:X3", + "customRepo:X4", + "customRepo:X6", + "securityRepo:R3", + "securityRepo:R4"); + + assertThat(context.testCheckClasses).extracting(Class::getSimpleName).containsExactly( + "Rule1", + "Rule3", + "Rule4", + "Rule6", + "Scanner"); + + assertThat(context.autoScanCompatibleRules).containsExactly(RuleKey.of("customRepo", "X1")); + } + + @Test + void should_fail_if_repository_key_is_blank() { + TestCheckRegistrarContext context = new TestCheckRegistrarContext(); + List checkClasses = List.of(Rule1.class); + assertThatThrownBy(() -> context.registerMainChecks(" ", checkClasses)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Please specify a non blank repository key"); + } + + @Test + void should_fail_if_not_a_JavaCheck() { + TestCheckRegistrarContext context = new TestCheckRegistrarContext(); + List checkClasses = List.of(Object.class); + assertThatThrownBy(() -> context.registerMainChecks("repo", checkClasses)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Fail to instantiate class java.lang.Object"); + } + +} diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/TestProfileRegistrarContextTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/TestProfileRegistrarContextTest.java new file mode 100644 index 00000000000..437d7ad8588 --- /dev/null +++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/TestProfileRegistrarContextTest.java @@ -0,0 +1,38 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2023 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.verifier; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.sonar.api.rule.RuleKey; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class TestProfileRegistrarContextTest { + + @Test + void store_registration() { + TestProfileRegistrarContext context = new TestProfileRegistrarContext(); + context.registerDefaultQualityProfileRules(List.of(RuleKey.of("java", "S1234"))); + assertThat(context.defaultQualityProfileRules).containsExactly(RuleKey.of("java", "S1234")); + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/SanityTest.java b/java-checks/src/test/java/org/sonar/java/checks/SanityTest.java index 930d4300536..af5529d7058 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/SanityTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/SanityTest.java @@ -305,7 +305,7 @@ private static SonarComponents sonarComponents(File moduleBaseDir, List> problemsToFilePaths = new HashMap<>(); private final CheckFactory checkFactory; + private final ActiveRules activeRules; @Nullable private final ProjectDefinition projectDefinition; private final FileSystem fs; @@ -116,13 +118,14 @@ public class SonarComponents { private final List> allChecks; private SensorContext context; private UnaryOperator> checkFilter = UnaryOperator.identity(); + private final Set additionalAutoScanCompatibleRuleKeys; private boolean alreadyLoggedSkipStatus = false; public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, - CheckFactory checkFactory) { - this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, null, null); + CheckFactory checkFactory, ActiveRules activeRules) { + this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, null); } /** @@ -130,8 +133,8 @@ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSyst */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, - @Nullable CheckRegistrar[] checkRegistrars) { - this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, checkRegistrars, null); + ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars) { + this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null); } /** @@ -139,8 +142,8 @@ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSyst */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, - @Nullable ProjectDefinition projectDefinition) { - this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, null, projectDefinition); + ActiveRules activeRules, @Nullable ProjectDefinition projectDefinition) { + this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules,null, projectDefinition); } /** @@ -148,35 +151,27 @@ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSyst */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, - @Nullable CheckRegistrar[] checkRegistrars, @Nullable ProjectDefinition projectDefinition) { + ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, + @Nullable ProjectDefinition projectDefinition) { this.fileLinesContextFactory = fileLinesContextFactory; this.fs = fs; this.javaClasspath = javaClasspath; this.javaTestClasspath = javaTestClasspath; this.checkFactory = checkFactory; + this.activeRules = activeRules; this.projectDefinition = projectDefinition; this.mainChecks = new ArrayList<>(); this.testChecks = new ArrayList<>(); this.jspChecks = new ArrayList<>(); this.allChecks = new ArrayList<>(); + this.additionalAutoScanCompatibleRuleKeys = new TreeSet<>(); if (checkRegistrars != null) { - CheckRegistrar.RegistrarContext registrarContext = new CheckRegistrar.RegistrarContext(); - for (CheckRegistrar checkClassesRegister : checkRegistrars) { - checkClassesRegister.register(registrarContext); - List> checkClasses = getChecks(registrarContext.checkClasses()); - List> testCheckClasses = getChecks(registrarContext.testCheckClasses()); - registerMainCheckClasses(registrarContext.repositoryKey(), checkClasses); - registerTestCheckClasses(registrarContext.repositoryKey(), testCheckClasses); + for (CheckRegistrar registrar : checkRegistrars) { + registrar.register(this); } } } - private static List> getChecks(@Nullable Iterable> iterable) { - return iterable != null ? - StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()) : - Collections.emptyList(); - } - public void setSensorContext(SensorContext context) { this.context = context; } @@ -233,21 +228,55 @@ private static File findPluginJar() { } } - public void registerMainCheckClasses(String repositoryKey, Iterable> checkClasses) { - registerCheckClasses(mainChecks, repositoryKey, checkClasses); + @Override + public void registerMainChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { + registerCheckClasses(mainChecks, repositoryKey, javaCheckClassesAndInstances); + } + + @Override + public void registerTestChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { + registerCheckClasses(testChecks, repositoryKey, javaCheckClassesAndInstances); + } + + @Override + public void registerMainSharedCheck(JavaCheck check, Collection ruleKeys) { + if (hasAtLeastOneActiveRule(ruleKeys)) { + mainChecks.add(check); + } + } + + @Override + public void registerTestSharedCheck(JavaCheck check, Collection ruleKeys) { + if (hasAtLeastOneActiveRule(ruleKeys)) { + testChecks.add(check); + } + } + + @Override + public void registerAutoScanCompatibleRules(Collection ruleKeys) { + additionalAutoScanCompatibleRuleKeys.addAll(ruleKeys); } - public void registerTestCheckClasses(String repositoryKey, Iterable> checkClasses) { - registerCheckClasses(testChecks, repositoryKey, checkClasses); + public Set getAdditionalAutoScanCompatibleRuleKeys() { + return additionalAutoScanCompatibleRuleKeys; } - private void registerCheckClasses(List destinationList, String repositoryKey, Iterable> checkClasses) { - Checks createdChecks = checkFactory.create(repositoryKey).addAnnotatedChecks(checkClasses); + private boolean hasAtLeastOneActiveRule(Collection ruleKeys) { + return ruleKeys.stream().anyMatch(ruleKey -> activeRules.find(ruleKey) != null); + } + + + private void registerCheckClasses(List destinationList, String repositoryKey, Collection javaCheckClassesAndInstances) { + Checks createdChecks = checkFactory.create(repositoryKey).addAnnotatedChecks(javaCheckClassesAndInstances); allChecks.add(createdChecks); Map, Integer> classIndexes = new HashMap<>(); int i = 0; - for (Class checkClass : checkClasses) { - classIndexes.put(checkClass, i); + for (Object javaCheckClassOrInstance : javaCheckClassesAndInstances) { + if (javaCheckClassOrInstance instanceof Class) { + classIndexes.put((Class) javaCheckClassOrInstance, i); + } else { + classIndexes.put(((JavaCheck) javaCheckClassOrInstance).getClass(), i); + } i++; } List orderedChecks = createdChecks.all().stream() diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/CheckRegistrar.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/CheckRegistrar.java index e761b7c11ea..feb445dd4d3 100644 --- a/java-frontend/src/main/java/org/sonar/plugins/java/api/CheckRegistrar.java +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/CheckRegistrar.java @@ -19,10 +19,16 @@ */ package org.sonar.plugins.java.api; -import org.sonar.java.Preconditions; -import org.sonar.java.annotations.Beta; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.sonar.api.batch.ScannerSide; +import org.sonar.api.rule.RuleKey; +import org.sonar.java.Preconditions; +import org.sonar.java.annotations.Beta; import org.sonarsource.api.sonarlint.SonarLintSide; /** @@ -53,8 +59,8 @@ public interface CheckRegistrar { */ class RegistrarContext { private String repositoryKey; - private Iterable> checkClasses; - private Iterable> testCheckClasses; + private Iterable> mainCheckClassList; + private Iterable> testCheckClassList; /** * Registers java checks for a given repository. @@ -65,14 +71,18 @@ class RegistrarContext { public void registerClassesForRepository(String repositoryKey, Iterable> checkClasses, Iterable> testCheckClasses) { Preconditions.checkArgument(StringUtils.isNotBlank(repositoryKey), "Please specify a valid repository key to register your custom rules"); this.repositoryKey = repositoryKey; - this.checkClasses = checkClasses; - this.testCheckClasses = testCheckClasses; + this.mainCheckClassList = checkClasses; + this.testCheckClassList = testCheckClasses; + registerMainChecks(repositoryKey, asCollection(checkClasses)); + registerTestChecks(repositoryKey, asCollection(testCheckClasses)); } /** * getter for repository key. * @return the repository key. + * @deprecated RegistrarContext should just forward the registration and not have any getters */ + @Deprecated(since = "7.25", forRemoval = true) public String repositoryKey() { return repositoryKey; } @@ -80,19 +90,78 @@ public String repositoryKey() { /** * get main source check classes * @return iterable of main checks classes + * @deprecated RegistrarContext should just forward the registration and not have any getters */ + @Deprecated(since = "7.25", forRemoval = true) public Iterable> checkClasses() { - return checkClasses; + return mainCheckClassList; } /** * get test source check classes * @return iterable of test checks classes + * @deprecated RegistrarContext should just forward the registration and not have any getters */ + @Deprecated(since = "7.25", forRemoval = true) public Iterable> testCheckClasses() { - return testCheckClasses; + return testCheckClassList; + } + + /** + * Registers main code java checks for a given repository. + * @param repositoryKey key of rule repository + * @param javaCheckClassesAndInstances a collection of Class and + * JavaCheck> instances + */ + public void registerMainChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { + // to be overridden + } + + /** + * Registers test code java checks for a given repository. + * @param repositoryKey key of rule repository + * @param javaCheckClassesAndInstances a collection of Class and + * JavaCheck> instances + */ + public void registerTestChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { + // to be overridden } + /** + * Registers one main code check related to not one but a list of rules. The check will be active if at least one + * of the given rule key is active. In this context injection of @RuleProperty and auto instantiation of rules + * defined as template in RulesDefinition will not work. And the reportIssue mechanism will not be able to find the + * RuleKey automatically. + */ + public void registerMainSharedCheck(JavaCheck check, Collection ruleKeys) { + // to be overridden + } + + /** + * Registers one test code check related to not one but a list of rules. The check will be active if at least one + * of the given rule key is active. In this context injection of @RuleProperty and auto instantiation of rules + * defined as template in RulesDefinition will not work. + */ + public void registerTestSharedCheck(JavaCheck check, Collection ruleKeys) { + // to be overridden + } + + /** + * Cannot be used outside of Sonar Products. Registers rules compatible with the autoscan context. + * Note: It's possible to convert checkClass to RuleKey using: + *
+     *   RuleKey.of(repositoryKey, RuleAnnotationUtils.getRuleKey(checkClass))
+     * 
+ */ + public void registerAutoScanCompatibleRules(Collection ruleKeys) { + // to be overridden + } + + private static Collection asCollection(@Nullable Iterable iterable) { + return iterable != null ? + StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()) : + Collections.emptyList(); + } } } diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/ProfileRegistrar.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/ProfileRegistrar.java new file mode 100644 index 00000000000..7e654534db1 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/ProfileRegistrar.java @@ -0,0 +1,65 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2023 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.plugins.java.api; + +import java.util.Collection; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.ServerSide; +import org.sonar.java.annotations.Beta; +import org.sonarsource.api.sonarlint.SonarLintSide; + +/** + * This class can be extended to provide additional rule keys in the builtin default quality profile. + * + *
+ *   {@code
+ *     public void register(RegistrarContext registrarContext) {
+ *       registrarContext.registerDefaultQualityProfileRules(ruleKeys);
+ *     }
+ *   }
+ * 
+ * + * Note: It's possible to convert checkClass to RuleKey using: + *
+ *   {@code
+ *     RuleKey.of(repositoryKey, RuleAnnotationUtils.getRuleKey(checkClass))
+ *   }
+ * 
+ */ +@Beta +@SonarLintSide +@ServerSide +public interface ProfileRegistrar { + + /** + * This method is called on server side and during an analysis to modify the builtin default quality profile for java. + */ + void register(RegistrarContext registrarContext); + + interface RegistrarContext { + + /** + * Registers additional rules into the "Sonar Way" default quality profile for the language "java". + */ + void registerDefaultQualityProfileRules(Collection ruleKeys); + + } + +} diff --git a/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java b/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java index 88a77239030..9670719fd7a 100644 --- a/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java +++ b/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java @@ -43,6 +43,7 @@ import org.sonar.api.SonarQubeSide; import org.sonar.api.SonarRuntime; import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.cache.ReadCache; @@ -816,7 +817,8 @@ private List scan(MapSettings settings, SonarRuntime sonarRuntime, Li javaClasspath = mock(ClasspathForMain.class); javaTestClasspath = mock(ClasspathForTest.class); - sonarComponents = new SonarComponents(fileLinesContextFactory, sensorContext.fileSystem(), javaClasspath, javaTestClasspath, mock(CheckFactory.class)); + sonarComponents = new SonarComponents(fileLinesContextFactory, sensorContext.fileSystem(), javaClasspath, javaTestClasspath, + mock(CheckFactory.class), mock(ActiveRules.class)); sonarComponents.setSensorContext(sensorContext); sonarComponents.mainChecks().add(mainCodeIssueScannerAndFilter); sonarComponents.testChecks().add(testCodeIssueScannerAndFilter); 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 9aab34e955a..dbcf03ae4d7 100644 --- a/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java @@ -161,7 +161,7 @@ void base_and_work_directories() { DefaultFileSystem fs = context.fileSystem(); fs.setWorkDir(workDir.toPath()); - SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, null, mock(ClasspathForTest.class), checkFactory); + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, null, mock(ClasspathForTest.class), checkFactory, context.activeRules()); assertThat(sonarComponents.projectLevelWorkDir()).isEqualTo(workDir); } @@ -177,7 +177,8 @@ void set_work_directory_using_project_definition() { parentProjectDefinition.setWorkDir(workDir); ProjectDefinition childProjectDefinition = ProjectDefinition.create(); parentProjectDefinition.addSubProject(childProjectDefinition); - SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, null, mock(ClasspathForTest.class), checkFactory, childProjectDefinition); + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, null, mock(ClasspathForTest.class), + checkFactory, context.activeRules(), childProjectDefinition); assertThat(sonarComponents.projectLevelWorkDir()).isEqualTo(workDir); } @@ -193,7 +194,8 @@ void test_sonar_components() { FileLinesContext fileLinesContext = mock(FileLinesContext.class); when(fileLinesContextFactory.createFor(any(InputFile.class))).thenReturn(fileLinesContext); - SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, null, javaTestClasspath, checkFactory); + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, null, javaTestClasspath, + checkFactory, context.activeRules()); sonarComponents.setSensorContext(sensorContextTester); List visitors = sonarComponents.mainChecks(); @@ -214,7 +216,8 @@ void test_sonar_components() { ClasspathForMain javaClasspath = mock(ClasspathForMain.class); List list = mock(List.class); when(javaClasspath.getElements()).thenReturn(list); - sonarComponents = new SonarComponents(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory); + sonarComponents = new SonarComponents(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, + checkFactory, context.activeRules()); assertThat(sonarComponents.getJavaClasspath()).isEqualTo(list); } @@ -225,7 +228,7 @@ void creation_of_custom_checks() { when(this.checks.all()).thenReturn(Collections.singletonList(expectedCheck)).thenReturn(new ArrayList<>()); SonarComponents sonarComponents = new SonarComponents(this.fileLinesContextFactory, null, null, - null, this.checkFactory, new CheckRegistrar[]{expectedRegistrar}); + null, this.checkFactory, context.activeRules(), new CheckRegistrar[]{expectedRegistrar}); sonarComponents.setSensorContext(context); List visitors = sonarComponents.mainChecks(); @@ -244,7 +247,7 @@ void creation_of_custom_test_checks() { when(checks.all()).thenReturn(new ArrayList<>()).thenReturn(Collections.singletonList(expectedCheck)); SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, - null, checkFactory, new CheckRegistrar[]{expectedRegistrar}); + null, checkFactory, context.activeRules(), new CheckRegistrar[]{expectedRegistrar}); sonarComponents.setSensorContext(context); List visitors = sonarComponents.mainChecks(); @@ -272,7 +275,7 @@ class CheckC implements JavaCheck { .thenReturn(Arrays.asList(new CheckA(), new CheckB(), new CheckC())) .thenReturn(Arrays.asList(new CheckA(), new CheckB(), new CheckC())); SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, - null, checkFactory, new CheckRegistrar[]{expectedRegistrar}); + null, checkFactory, context.activeRules(), new CheckRegistrar[]{expectedRegistrar}); sonarComponents.setSensorContext(context); List mainChecks = sonarComponents.mainChecks(); @@ -299,7 +302,7 @@ class CheckC implements JavaCheck { .thenReturn(Arrays.asList(new CheckA(), new CheckB(), new CheckC())) .thenReturn(Arrays.asList(new CheckC(), new CheckB(), new CheckA())); SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, - null, checkFactory, new CheckRegistrar[]{expectedRegistrar}); + null, checkFactory, context.activeRules(), new CheckRegistrar[]{expectedRegistrar}); sonarComponents.setSensorContext(context); sonarComponents.setCheckFilter(checks -> checks.stream() .filter(c -> !c.getClass().getSimpleName().equals("CheckB")).collect(Collectors.toList())); @@ -323,7 +326,7 @@ void creation_of_both_types_test_checks() { when(this.checks.all()).thenReturn(Collections.singletonList(expectedCheck)).thenReturn(Collections.singletonList(expectedTestCheck)); SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, - null, checkFactory, new CheckRegistrar[]{expectedRegistrar}); + null, checkFactory, context.activeRules(), new CheckRegistrar[]{expectedRegistrar}); sonarComponents.setSensorContext(context); List visitors = sonarComponents.mainChecks(); @@ -336,6 +339,94 @@ void creation_of_both_types_test_checks() { postTestExecutionChecks(); } + @Test + void register_shared_check_when_rules_are_active_or_not() { + ActiveRules activeRules = activeRules("java:S101", "java:S102"); + CheckFactory checkFactory = new CheckFactory(activeRules); + SensorContextTester context = SensorContextTester.create(new File(".")).setActiveRules(activeRules); + + class RuleA implements JavaCheck { + } + class RuleB implements JavaCheck { + } + class RuleC implements JavaCheck { + } + class RuleD implements JavaCheck { + } + class RuleE implements JavaCheck { + } + class RuleF implements JavaCheck { + } + + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, + null, checkFactory, activeRules, new CheckRegistrar[] { + ctx -> ctx.registerMainSharedCheck(new RuleA(), ruleKeys("java:S404", "java:S102")), + ctx -> ctx.registerMainSharedCheck(new RuleB(), ruleKeys("java:S404", "java:S500")), + ctx -> ctx.registerMainSharedCheck(new RuleC(), ruleKeys("java:S101", "java:S102")), + ctx -> ctx.registerTestSharedCheck(new RuleD(), ruleKeys("java:S404", "java:S405", "java:S406")), + ctx -> ctx.registerTestSharedCheck(new RuleE(), ruleKeys("java:S102")), + ctx -> ctx.registerTestSharedCheck(new RuleF(), List.of()) + }); + sonarComponents.setSensorContext(context); + + assertThat(sonarComponents.mainChecks()) + .extracting(c -> c.getClass().getSimpleName()) + .containsExactly("RuleA", "RuleC"); + assertThat(sonarComponents.testChecks()) + .extracting(c -> c.getClass().getSimpleName()) + .containsExactly("RuleE"); + } + + @Test + void register_custom_rule_by_instances_instead_of_classes() { + ActiveRules activeRules = activeRules("java:S101", "java:S102"); + CheckFactory checkFactory = new CheckFactory(activeRules); + SensorContextTester context = SensorContextTester.create(new File(".")).setActiveRules(activeRules); + @Rule(key = "S101") + class RuleA implements JavaCheck { + } + @Rule(key = "S102") + class RuleB implements JavaCheck { + } + @Rule(key = "S103") + class RuleC implements JavaCheck { + } + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, + null, checkFactory, activeRules, new CheckRegistrar[] { + ctx -> ctx.registerMainChecks("java", List.of( + new RuleA(), + new RuleC())), + ctx -> ctx.registerTestChecks("java", List.of( + new RuleB())) + }); + sonarComponents.setSensorContext(context); + + assertThat(sonarComponents.mainChecks()) + .extracting(c -> c.getClass().getSimpleName()) + .containsExactly("RuleA"); + assertThat(sonarComponents.testChecks()) + .extracting(c -> c.getClass().getSimpleName()) + .containsExactly("RuleB"); + } + + @Test + void auto_scan_compatible_rules() { + ActiveRules activeRules = activeRules(); + CheckFactory checkFactory = new CheckFactory(activeRules); + SensorContextTester context = SensorContextTester.create(new File(".")).setActiveRules(activeRules); + + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, + null, checkFactory, activeRules, new CheckRegistrar[] { + ctx -> ctx.registerAutoScanCompatibleRules(ruleKeys("java:S101", "java:S102")), + ctx -> ctx.registerAutoScanCompatibleRules(ruleKeys("javabugs:S200")) + }); + sonarComponents.setSensorContext(context); + + assertThat(sonarComponents.getAdditionalAutoScanCompatibleRuleKeys()) + .extracting(RuleKey::toString) + .containsExactlyInAnyOrder("java:S101", "java:S102", "javabugs:S200"); + } + @Test void no_issue_when_check_not_found() throws Exception { JavaCheck expectedCheck = new CustomCheck(); @@ -343,7 +434,7 @@ void no_issue_when_check_not_found() throws Exception { when(this.checks.ruleKey(any(JavaCheck.class))).thenReturn(null); SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, - null, checkFactory, new CheckRegistrar[]{expectedRegistrar}); + null, checkFactory, context.activeRules(), new CheckRegistrar[]{expectedRegistrar}); sonarComponents.setSensorContext(context); sonarComponents.addIssue(TestUtils.emptyInputFile("file.java"), expectedCheck, 0, "message", null); @@ -372,7 +463,7 @@ void add_issue_or_parse_error() throws Exception { when(this.checks.ruleKey(any(JavaCheck.class))).thenReturn(mock(RuleKey.class)); SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fileSystem, null, - null, checkFactory, new CheckRegistrar[]{expectedRegistrar}); + null, checkFactory, context.activeRules(), new CheckRegistrar[]{expectedRegistrar}); sonarComponents.setSensorContext(context); sonarComponents.addIssue(inputFile, expectedCheck, -5, "message on wrong line", null); @@ -409,7 +500,8 @@ void fail_on_empty_location() { + "}\n").build(); SensorContextTester context = SensorContextTester.create(new File("")); - SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, context.fileSystem(), null, null, checkFactory, new CheckRegistrar[]{expectedRegistrar}); + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, context.fileSystem(), null, null, + checkFactory, context.activeRules(), new CheckRegistrar[]{expectedRegistrar}); sonarComponents.setSensorContext(context); AnalyzerMessage.TextSpan emptyTextSpan = new AnalyzerMessage.TextSpan(3, 10, 3, 10); @@ -427,7 +519,7 @@ void fail_on_empty_location() { @Test void cancellation() { - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null); SensorContextTester context = SensorContextTester.create(new File("")); sonarComponents.setSensorContext(context); @@ -443,7 +535,7 @@ void cancellation() { @Test void knows_if_quickfixes_are_supported() { SensorContextTester context = SensorContextTester.create(new File("")); - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null); sonarComponents.setSensorContext(context); SonarRuntime sonarQube = SonarRuntimeImpl.forSonarQube(V8_9, SonarQubeSide.SCANNER, SonarEdition.COMMUNITY); @@ -463,7 +555,7 @@ void knows_if_quickfixes_are_supported() { @Test void knows_if_quickfixes_can_be_advertised() { SensorContextTester context = SensorContextTester.create(new File("")); - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null); sonarComponents.setSensorContext(context); assertTrue(sonarComponents.isSetQuickFixAvailableCompatible()); @@ -473,7 +565,7 @@ void knows_if_quickfixes_can_be_advertised() { void knows_if_quickfixes_can_not_be_advertised() { SensorContextTester context = SensorContextTester.create(new File("")); context.setRuntime(SonarRuntimeImpl.forSonarQube(Version.create(9, 0), SonarQubeSide.SERVER, SonarEdition.COMMUNITY)); - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null); sonarComponents.setSensorContext(context); assertFalse(sonarComponents.isSetQuickFixAvailableCompatible()); @@ -488,7 +580,7 @@ void readFileContentFromInputFile() throws Exception { DefaultFileSystem fileSystem = context.fileSystem(); fileSystem.add(inputFile); fileSystem.setEncoding(StandardCharsets.ISO_8859_1); - SonarComponents sonarComponents = new SonarComponents(null, fileSystem, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, fileSystem, null, null, null, null); context.setRuntime(SonarRuntimeImpl.forSonarLint(V8_9)); sonarComponents.setSensorContext(context); @@ -513,7 +605,7 @@ void io_error_when_reading_file_should_fail_analysis() { InputFile unknownInputFile = TestUtils.emptyInputFile("unknown_file.java"); fileSystem.add(unknownInputFile); context.setRuntime(SonarRuntimeImpl.forSonarLint(V8_9)); - SonarComponents sonarComponents = new SonarComponents(null, fileSystem, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, fileSystem, null, null, null, null); sonarComponents.setSensorContext(context); try { @@ -544,7 +636,8 @@ void jsp_classpath_should_include_plugin() throws Exception { when(javaClasspath.getElements()).thenReturn(Collections.singletonList(someJar)); File plugin = new File("target/classes"); - SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, javaClasspath, mock(ClasspathForTest.class), checkFactory); + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, javaClasspath, mock(ClasspathForTest.class), + checkFactory, context.activeRules()); List jspClassPath = sonarComponents.getJspClasspath().stream().map(File::getAbsolutePath).collect(Collectors.toList()); assertThat(jspClassPath).containsExactly(plugin.getAbsolutePath(), someJar.getAbsolutePath()); } @@ -552,7 +645,7 @@ void jsp_classpath_should_include_plugin() throws Exception { @Test void autoscan_getters() { MapSettings settings = new MapSettings(); - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null); sonarComponents.setSensorContext(SensorContextTester.create(new File("")).setSettings(settings)); // default value @@ -587,7 +680,7 @@ void autoscan_getters() { @Test void batch_getters() { MapSettings settings = new MapSettings(); - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null); sonarComponents.setSensorContext(SensorContextTester.create(new File("")).setSettings(settings)); // default value @@ -631,7 +724,7 @@ void batch_getters() { void batch_size_dynamic_computation(long maxMemoryMB, long expectedBatchSizeKB) { long maxMemoryBytes = maxMemoryMB * 1_000_000; MapSettings settings = new MapSettings(); - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null); sonarComponents.setSensorContext(SensorContextTester.create(new File("")).setSettings(settings)); LongSupplier oldValue = SonarComponents.maxMemoryInBytesProvider; @@ -644,7 +737,7 @@ void batch_size_dynamic_computation(long maxMemoryMB, long expectedBatchSizeKB) @Test void file_by_file_getters() { MapSettings settings = new MapSettings(); - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, null, null); sonarComponents.setSensorContext(SensorContextTester.create(new File("")).setSettings(settings)); // default value @@ -669,8 +762,8 @@ void skipUnchangedFiles_returns_result_from_context() throws ApiMismatchExceptio sensorContextTester.fileSystem(), mock(ClasspathForMain.class), mock(ClasspathForTest.class), - checkFactory - ); + checkFactory, + context.activeRules()); IncrementalAnalysisSensorContext context = mock(IncrementalAnalysisSensorContext.class); when(context.canSkipUnchangedFiles()).thenReturn(true); @@ -691,8 +784,8 @@ void skipUnchangedFiles_returns_false_by_default() throws ApiMismatchException { sensorContextTester.fileSystem(), mock(ClasspathForMain.class), mock(ClasspathForTest.class), - checkFactory - ); + checkFactory, + context.activeRules()); assertThat(sonarComponents.canSkipUnchangedFiles()).isFalse(); } @@ -705,8 +798,8 @@ void skipUnchangedFiles_throws_a_NoSuchMethodError_when_canSkipUnchangedFiles_no sensorContextTester.fileSystem(), mock(ClasspathForMain.class), mock(ClasspathForTest.class), - checkFactory - ); + checkFactory, + context.activeRules()); IncrementalAnalysisSensorContext context = mock(IncrementalAnalysisSensorContext.class); when(context.canSkipUnchangedFiles()).thenThrow(new NoSuchMethodError("API version mismatch :-(")); @@ -728,9 +821,8 @@ void fileCanBeSkipped_returns_false_when_the_file_is_a_generated_file() throws A sensorContextTester.fileSystem(), mock(ClasspathForMain.class), mock(ClasspathForTest.class), - checkFactory - ) - ); + checkFactory, + context.activeRules())); SensorContext contextMock = mock(SensorContext.class); sonarComponents.setSensorContext(contextMock); @@ -825,8 +917,7 @@ private static Stream provideInputsFor_canSkipUnchangedFiles() { Arguments.of(false, false, false), Arguments.of(false, true, false), Arguments.of(true, false, true), - Arguments.of(true, true, true) - ); + Arguments.of(true, true, true)); } @ParameterizedTest @@ -838,8 +929,8 @@ void canSkipUnchangedFiles(@CheckForNull Boolean overrideFlagVal, @CheckForNull sensorContextTester.fileSystem(), mock(ClasspathForMain.class), mock(ClasspathForTest.class), - checkFactory - ); + checkFactory, + context.activeRules()); IncrementalAnalysisSensorContext context = mock(IncrementalAnalysisSensorContext.class); Configuration config = mock(Configuration.class); @@ -881,7 +972,7 @@ class Logging { @BeforeEach void beforeEach() { - sonarComponents = new SonarComponents(null, fs, javaClasspath, javaTestClasspath, null); + sonarComponents = new SonarComponents(null, fs, javaClasspath, javaTestClasspath, null, null); sonarComponents.setSensorContext(context); } @@ -1057,19 +1148,20 @@ void should_return_generated_code_visitors() throws Exception { CheckFactory checkFactory = new CheckFactory(activeRules); JspCodeCheck check = new JspCodeCheck(); - SonarComponents sonarComponents = new SonarComponents(null, null, null, null, checkFactory, new CheckRegistrar[]{getRegistrar(check)}); + SonarComponents sonarComponents = new SonarComponents(null, null, null, null, + checkFactory, context.activeRules(), new CheckRegistrar[] {getRegistrar(check)}); List checks = sonarComponents.jspChecks(); assertThat(checks) .isNotEmpty() .allMatch(JspCodeCheck.class::isInstance); - sonarComponents = new SonarComponents(null, null, null, null, checkFactory); + sonarComponents = new SonarComponents(null, null, null, null, checkFactory, context.activeRules()); assertThat(sonarComponents.jspChecks()).isEmpty(); } @Test void moduleKey_empty() { - var sonarComponents = new SonarComponents(null, null, null, null, null); + var sonarComponents = new SonarComponents(null, null, null, null, null, null); assertThat(sonarComponents.getModuleKey()).isEmpty(); } @@ -1096,4 +1188,21 @@ public static class JspCodeCheck implements JspCodeVisitor { interface IncrementalAnalysisSensorContext extends SensorContext { boolean canSkipUnchangedFiles(); } + + private List ruleKeys(String... repositoryAndKeys) { + return Arrays.stream(repositoryAndKeys) + .map(RuleKey::parse) + .collect(Collectors.toList()); + } + + private static ActiveRules activeRules(String... repositoryAndKeys) { + ActiveRulesBuilder activeRules = new ActiveRulesBuilder(); + for (String repositoryAndKey : repositoryAndKeys) { + activeRules.addRule(new NewActiveRule.Builder() + .setRuleKey(RuleKey.parse(repositoryAndKey)) + .setLanguage("java") + .build()); + } + return activeRules.build(); + } } 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 51bc93723b9..10cd6bd7553 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 @@ -234,7 +234,7 @@ void should_interrupt_analysis_when_is_cancelled() throws Exception { @Test void should_swallow_log_and_report_checks_exceptions() { JavaAstScanner scanner = new JavaAstScanner(null); - SonarComponents sonarComponent = new SonarComponents(null, context.fileSystem(), null, null, null); + SonarComponents sonarComponent = new SonarComponents(null, context.fileSystem(), null, null, null, null); sonarComponent.setSensorContext(context); scanner.setVisitorBridge(new VisitorsBridge(Collections.singleton(new CheckThrowingException(new NullPointerException("foo"))), new ArrayList<>(), sonarComponent)); InputFile scannedFile = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt"); @@ -441,7 +441,7 @@ private void scanFilesWithVisitors(List inputFiles, List(), sonarComponents, new JavaVersionImpl(javaVersion)); diff --git a/java-frontend/src/test/java/org/sonar/java/ast/visitors/SonarSymbolTableVisitorTest.java b/java-frontend/src/test/java/org/sonar/java/ast/visitors/SonarSymbolTableVisitorTest.java index d297b2417a2..f8e542e9877 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/visitors/SonarSymbolTableVisitorTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/visitors/SonarSymbolTableVisitorTest.java @@ -34,6 +34,7 @@ import org.sonar.api.batch.fs.TextPointer; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.fs.internal.DefaultTextPointer; +import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.batch.sensor.internal.SensorContextTester; import org.sonar.api.measures.FileLinesContextFactory; @@ -60,7 +61,7 @@ class SonarSymbolTableVisitorTest { public void setUp() { context = SensorContextTester.create(temp.getRoot()); sonarComponents = new SonarComponents(mock(FileLinesContextFactory.class), context.fileSystem(), - mock(ClasspathForMain.class), mock(ClasspathForTest.class), mock(CheckFactory.class)); + mock(ClasspathForMain.class), mock(ClasspathForTest.class), mock(CheckFactory.class), mock(ActiveRules.class)); sonarComponents.setSensorContext(context); } diff --git a/java-frontend/src/test/java/org/sonar/java/ast/visitors/SyntaxHighlighterVisitorTest.java b/java-frontend/src/test/java/org/sonar/java/ast/visitors/SyntaxHighlighterVisitorTest.java index 351bb1515dd..1d680e04428 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/visitors/SyntaxHighlighterVisitorTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/visitors/SyntaxHighlighterVisitorTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.junit.rules.TemporaryFolder; import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.batch.sensor.highlighting.TypeOfText; import org.sonar.api.batch.sensor.internal.SensorContextTester; @@ -67,7 +68,7 @@ class SyntaxHighlighterVisitorTest { public void setUp() throws Exception { context = SensorContextTester.create(temp.getRoot()); sonarComponents = new SonarComponents(mock(FileLinesContextFactory.class), context.fileSystem(), - mock(ClasspathForMain.class), mock(ClasspathForTest.class), mock(CheckFactory.class)); + mock(ClasspathForMain.class), mock(ClasspathForTest.class), mock(CheckFactory.class), mock(ActiveRules.class)); sonarComponents.setSensorContext(context); syntaxHighlighterVisitor = new SyntaxHighlighterVisitor(sonarComponents); } 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 d2e91c65ff5..bb50f96eeb4 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 @@ -76,7 +76,7 @@ class DefaultJavaFileScannerContextWithSensorContextTest { @BeforeEach void setup() throws IOException { sensorContext = SensorContextTester.create(Paths.get("")); - SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, sensorContext.fileSystem(), javaClasspath, javaTestClasspath, checkFactory); + SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, sensorContext.fileSystem(), javaClasspath, javaTestClasspath, checkFactory, sensorContext.activeRules()); sonarComponents.setSensorContext(sensorContext); // spy getRuleKey call, to avoid mocking CheckFactory and Checks diff --git a/java-frontend/src/test/java/org/sonar/java/model/DefaultModuleScannerContextTest.java b/java-frontend/src/test/java/org/sonar/java/model/DefaultModuleScannerContextTest.java index fa5d15df014..30e9a96f9e1 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/DefaultModuleScannerContextTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/DefaultModuleScannerContextTest.java @@ -43,7 +43,7 @@ class DefaultModuleScannerContextTest { @Test void test_addIssueOnProject_delegates_to_SonarComponents() { var sonarComponents = spy( - new SonarComponents(null, null, null, null, null) + new SonarComponents(null, null, null, null, null, null) ); var inputComponent = mock(InputComponent.class); doReturn(inputComponent).when(sonarComponents).project(); @@ -100,7 +100,7 @@ void test_inAndroidContext_returns_expected_version() { @Test void test_getProject_delegates_to_SonarComponents() { var sonarComponents = spy( - new SonarComponents(null, null, null, null, null) + new SonarComponents(null, null, null, null, null, null) ); var inputComponent = mock(InputComponent.class); doReturn(inputComponent).when(sonarComponents).project(); @@ -119,7 +119,7 @@ void test_getProject_delegates_to_SonarComponents() { @Test void test_getWorkingDirectory_delegates_to_SonarComponents() { var sonarComponents = spy( - new SonarComponents(null, null, null, null, null) + new SonarComponents(null, null, null, null, null, null) ); var expectedWorkDir = mock(File.class); doReturn(expectedWorkDir).when(sonarComponents).projectLevelWorkDir(); 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 a374b820af2..4b34af50d15 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 @@ -497,7 +497,7 @@ void scanWithoutParsing_returns_false_when_the_file_cannot_be_skipped() throws A void scanWithoutParsing_returns_false_when_the_file_is_a_generated_file() throws ApiMismatchException { InputFile inputFile = new GeneratedFile(Path.of("non-existing-generated-file.java")); - SonarComponents sonarComponents = spy(new SonarComponents(null, null, null, null, null)); + SonarComponents sonarComponents = spy(new SonarComponents(null, null, null, null, null, null)); SensorContext contextMock = mock(SensorContext.class); sonarComponents.setSensorContext(contextMock); @@ -639,7 +639,7 @@ private final VisitorsBridge visitorsBridge(Collection visitors SensorContextTester sensorContextTester = SensorContextTester.create(new File("")); sensorContextTester.setSettings(new MapSettings().setProperty(SonarComponents.FAIL_ON_EXCEPTION_KEY, failOnException)); - sonarComponents = new SonarComponents(null, null, null, null, null); + sonarComponents = new SonarComponents(null, null, null, null, null, null); sonarComponents.setSensorContext(sensorContextTester); VisitorsBridge visitorsBridge = new VisitorsBridge(visitors, new ArrayList<>(), sonarComponents); 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 c4a9d2d421b..18f8b7bd7f8 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 @@ -71,7 +71,7 @@ static void setup() { DefaultFileSystem fileSystem = sensorContext.fileSystem(); fileSystem.add(inputFile); - SonarComponents sonarComponents = new SonarComponents(null, fileSystem, null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, fileSystem, null, null, null, null); context = new JavaFileScannerContextForTests(cut, inputFile, cut.sema, sonarComponents, javaVersion, false, false, null); } 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 6593d4f5b0a..bbee4ac7447 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 @@ -45,7 +45,7 @@ class VisitorsBridgeForTestsTest { @Test void test_semantic_disabled() { SensorContextTester context = SensorContextTester.create(new File("")).setRuntime(SonarRuntimeImpl.forSonarLint(Version.create(6, 7))); - SonarComponents sonarComponents = new SonarComponents(null, context.fileSystem(), null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, context.fileSystem(), null, null, null, null); sonarComponents.setSensorContext(context); Tree parse = JParserTestUtils.parse("class A{}"); @@ -64,7 +64,7 @@ void test_semantic_disabled() { @Test void test_report_with_analysis_message() { SensorContextTester context = SensorContextTester.create(new File("")).setRuntime(SonarRuntimeImpl.forSonarLint(Version.create(6, 7))); - SonarComponents sonarComponents = new SonarComponents(null, context.fileSystem(), null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, context.fileSystem(), null, null, null, null); sonarComponents.setSensorContext(context); Tree parse = JParserTestUtils.parse("class A{}"); @@ -88,7 +88,7 @@ void test_report_with_analysis_message() { @Test void create_InputFileScannerContext_also_sets_testContext_field() { SensorContextTester context = SensorContextTester.create(new File("")).setRuntime(SonarRuntimeImpl.forSonarLint(Version.create(6, 7))); - SonarComponents sonarComponents = new SonarComponents(null, context.fileSystem(), null, null, null); + SonarComponents sonarComponents = new SonarComponents(null, context.fileSystem(), null, null, null, null); sonarComponents.setSensorContext(context); DummyVisitor javaCheck = new DummyVisitor(); VisitorsBridgeForTests visitorsBridgeForTests = new VisitorsBridgeForTests(Collections.singletonList(javaCheck), sonarComponents, new JavaVersionImpl()); diff --git a/java-frontend/src/test/java/org/sonar/plugins/java/api/CheckRegistrarTest.java b/java-frontend/src/test/java/org/sonar/plugins/java/api/CheckRegistrarTest.java index dd723986847..9e405b23cb5 100644 --- a/java-frontend/src/test/java/org/sonar/plugins/java/api/CheckRegistrarTest.java +++ b/java-frontend/src/test/java/org/sonar/plugins/java/api/CheckRegistrarTest.java @@ -19,28 +19,111 @@ */ package org.sonar.plugins.java.api; -import org.junit.jupiter.api.Test; - -import java.util.Collections; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.sonar.api.rule.RuleKey; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class CheckRegistrarTest { + TestInternalRegistration registrarContext = new TestInternalRegistration(); + @Test - void repository_key_is_mandatory() throws Exception { - CheckRegistrar.RegistrarContext registrarContext = new CheckRegistrar.RegistrarContext(); - List> checkClasses = Collections.emptyList(); - List> testCheckClasses = Collections.emptyList(); - try { - registrarContext.registerClassesForRepository(" ", checkClasses, testCheckClasses); - fail(""); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessage("Please specify a valid repository key to register your custom rules"); - } catch (Exception e) { - fail(""); + void repository_key_is_mandatory() { + List> emptyList = emptyList(); + assertThatThrownBy(() -> registrarContext.registerClassesForRepository(" ", emptyList, emptyList)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Please specify a valid repository key to register your custom rules"); + } + + @Test + void main_and_test_classes_registration() { + class MainCheckA implements JavaCheck { + } + class MainCheckB implements JavaCheck { } + class MainCheckC implements JavaCheck { + } + class TestCheckD implements JavaCheck { + } + class TestCheckE implements JavaCheck { + } + class Scanner implements JavaCheck { + } + + registrarContext.registerClassesForRepository("my-repo", null, null); + registrarContext.registerClassesForRepository("my-repo", + List.of(MainCheckA.class, MainCheckB.class), + List.of(TestCheckD.class)); + registrarContext.registerMainChecks("my-repo", + List.of(MainCheckC.class)); + registrarContext.registerTestChecks("my-repo", + List.of(TestCheckE.class)); + registrarContext.registerMainSharedCheck(new Scanner(), + List.of(RuleKey.of("repo", "S123"))); + registrarContext.registerTestSharedCheck(new Scanner(), + List.of(RuleKey.of("repo", "S456"))); + registrarContext.registerAutoScanCompatibleRules(List.of( + RuleKey.of("repo", "S123"), + RuleKey.of("repo", "S456"))); + + assertThat(registrarContext.repositoryKey()).isEqualTo("my-repo"); + assertThat(registrarContext.checkClasses()).hasSize(2); + assertThat(registrarContext.testCheckClasses()).hasSize(1); + + assertThat(registrarContext.events).containsExactly( + "register {} main checks in repository my-repo", + "register {} test checks in repository my-repo", + "register {MainCheckA, MainCheckB} main checks in repository my-repo", + "register {TestCheckD} test checks in repository my-repo", + "register {MainCheckC} main checks in repository my-repo", + "register {TestCheckE} test checks in repository my-repo", + "register Scanner for 1 main rules.", + "register Scanner for 1 test rules.", + "register 2 autoscan rules."); + } + + private static class TestInternalRegistration extends CheckRegistrar.RegistrarContext { + + public final List events = new ArrayList<>(); + + @Override + public void registerMainChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { + super.registerMainChecks(repositoryKey, javaCheckClassesAndInstances); + String names = javaCheckClassesAndInstances.stream().map(o -> ((Class) o).getSimpleName()).collect(Collectors.joining(", ")); + events.add("register {" + names + "} main checks in repository " + repositoryKey); + } + + @Override + public void registerTestChecks(String repositoryKey, Collection javaCheckClassesAndInstances) { + super.registerTestChecks(repositoryKey, javaCheckClassesAndInstances); + String names = javaCheckClassesAndInstances.stream().map(o -> ((Class) o).getSimpleName()).collect(Collectors.joining(", ")); + events.add("register {" + names + "} test checks in repository " + repositoryKey); + } + + @Override + public void registerMainSharedCheck(JavaCheck check, Collection ruleKeys) { + super.registerMainSharedCheck(check, ruleKeys); + events.add("register " + check.getClass().getSimpleName() + " for " + ruleKeys.size() + " main rules."); + } + + @Override + public void registerTestSharedCheck(JavaCheck check, Collection ruleKeys) { + super.registerTestSharedCheck(check, ruleKeys); + events.add("register " + check.getClass().getSimpleName() + " for " + ruleKeys.size() + " test rules."); + } + + @Override + public void registerAutoScanCompatibleRules(Collection ruleKeys) { + super.registerAutoScanCompatibleRules(ruleKeys); + events.add("register " + ruleKeys.size() + " autoscan rules."); + } + } } diff --git a/java-symbolic-execution/src/test/java/org/sonar/java/se/JavaFrontendIntegrationTest.java b/java-symbolic-execution/src/test/java/org/sonar/java/se/JavaFrontendIntegrationTest.java index 2f11666171b..d66e59b2175 100644 --- a/java-symbolic-execution/src/test/java/org/sonar/java/se/JavaFrontendIntegrationTest.java +++ b/java-symbolic-execution/src/test/java/org/sonar/java/se/JavaFrontendIntegrationTest.java @@ -35,6 +35,7 @@ import org.mockito.Mockito; import org.slf4j.event.Level; import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.batch.sensor.internal.SensorContextTester; import org.sonar.api.batch.sensor.issue.Issue; @@ -88,7 +89,8 @@ void setup() throws IOException { context.fileSystem(), Mockito.mock(ClasspathForMain.class), Mockito.mock(ClasspathForTest.class), - Mockito.mock(CheckFactory.class)); + Mockito.mock(CheckFactory.class), + Mockito.mock(ActiveRules.class)); sonarComponents.setSensorContext(context); inputFile = SETestUtils.inputFile("src/test/files/se/SimpleClass.java"); diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java index 17ddb52c50a..7e2d0c9d999 100644 --- a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -39,8 +40,7 @@ import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.config.Configuration; import org.sonar.api.issue.NoSonarFilter; -import org.sonar.api.utils.AnnotationUtils; -import org.sonar.check.Rule; +import org.sonar.api.rule.RuleKey; import org.sonar.java.JavaFrontend; import org.sonar.java.Measurer; import org.sonar.java.SonarComponents; @@ -57,6 +57,8 @@ import org.sonar.plugins.java.api.JavaVersion; import org.sonarsource.performance.measure.PerformanceMeasure; +import static org.sonar.api.rules.RuleAnnotationUtils.getRuleKey; + @Phase(name = Phase.Name.PRE) public class JavaSensor implements Sensor { @@ -90,8 +92,8 @@ public JavaSensor(SonarComponents sonarComponents, FileSystem fs, JavaResourceLo this.settings = settings; this.postAnalysisIssueFilter = postAnalysisIssueFilter; this.jasper = jasper; - this.sonarComponents.registerMainCheckClasses(CheckList.REPOSITORY_KEY, CheckList.getJavaChecks()); - this.sonarComponents.registerTestCheckClasses(CheckList.REPOSITORY_KEY, CheckList.getJavaTestChecks()); + this.sonarComponents.registerMainChecks(CheckList.REPOSITORY_KEY, CheckList.getJavaChecks()); + this.sonarComponents.registerTestChecks(CheckList.REPOSITORY_KEY, CheckList.getJavaTestChecks()); } @Override @@ -115,25 +117,24 @@ public void execute(SensorContext context) { sensorDuration.stop(); } - private static UnaryOperator> createCheckFilter(boolean isAutoScanCheckFiltering) { + private UnaryOperator> createCheckFilter(boolean isAutoScanCheckFiltering) { if (isAutoScanCheckFiltering) { - Set sonarWayRuleKeys = JavaSonarWayProfile.ruleKeys(); - Set> notWorkingChecks = CheckList.getJavaChecksNotWorkingForAutoScan(); + Set autoScanCompatibleRules = new HashSet<>(JavaSonarWayProfile.sonarJavaSonarWayRuleKeys()); + + CheckList.getJavaChecksNotWorkingForAutoScan().stream() + .map(checkClass -> RuleKey.of(CheckList.REPOSITORY_KEY, getRuleKey(checkClass))) + .forEach(autoScanCompatibleRules::remove); + + autoScanCompatibleRules.addAll(sonarComponents.getAdditionalAutoScanCompatibleRuleKeys()); + return checks -> checks.stream() - .filter(c -> sonarWayRuleKeys.contains(getKeyFromCheck(c))) - .filter(c -> !notWorkingChecks.contains(c.getClass())) + .filter(check -> sonarComponents.getRuleKey(check).map(autoScanCompatibleRules::contains).orElse(false)) .collect(Collectors.toList()); } else { return UnaryOperator.identity(); } } - @VisibleForTesting - static String getKeyFromCheck(JavaCheck check) { - Rule ruleAnnotation = AnnotationUtils.getAnnotation(check.getClass(), Rule.class); - return ruleAnnotation != null ? ruleAnnotation.key() : ""; - } - private static PerformanceMeasure.Duration createPerformanceMeasureReport(SensorContext context) { return PerformanceMeasure.reportBuilder() .activate(context.config().get(PERFORMANCE_MEASURE_ACTIVATION_PROPERTY).filter("true"::equals).isPresent()) diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSonarWayProfile.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSonarWayProfile.java index 66054b1a227..94c9195a9bc 100644 --- a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSonarWayProfile.java +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSonarWayProfile.java @@ -24,12 +24,14 @@ import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.rule.RuleKey; import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition; import org.sonar.java.annotations.VisibleForTesting; import org.sonar.java.checks.CheckList; +import org.sonar.plugins.java.api.ProfileRegistrar; import org.sonarsource.analyzer.commons.BuiltInQualityProfileJsonLoader; import org.sonarsource.api.sonarlint.SonarLintSide; @@ -47,23 +49,46 @@ public class JavaSonarWayProfile implements BuiltInQualityProfilesDefinition { static final String SECURITY_RULE_KEYS_METHOD_NAME = "getSecurityRuleKeys"; static final String DBD_RULE_KEYS_METHOD_NAME = "getDataflowBugDetectionRuleKeys"; static final String GET_REPOSITORY_KEY = "getRepositoryKey"; + static final String SECURITY_REPOSITORY_KEY = "javasecurity"; + static final String DBD_REPOSITORY_KEY = "javabugs"; static final String SONAR_WAY_PATH = "/org/sonar/l10n/java/rules/java/Sonar_way_profile.json"; + private final ProfileRegistrar[] profileRegistrars; + + public JavaSonarWayProfile(@Nullable ProfileRegistrar[] profileRegistrars) { + this.profileRegistrars = profileRegistrars; + } @Override public void define(Context context) { NewBuiltInQualityProfile sonarWay = context.createBuiltInQualityProfile("Sonar way", Java.KEY); + Set ruleKeys = new HashSet<>(sonarJavaSonarWayRuleKeys()); + if (profileRegistrars != null) { + for (ProfileRegistrar profileRegistrar : profileRegistrars) { + profileRegistrar.register(ruleKeys::addAll); + } + } - BuiltInQualityProfileJsonLoader.load(sonarWay, CheckList.REPOSITORY_KEY, SONAR_WAY_PATH); + // Former activation mechanism, it should be removed once sonar-security and sonar-dataflow-bug-detection + // support the new mechanism: + // registrarContext.internal().registerDefaultQualityProfileRules(ruleKeys); + // For now, it still uses reflexion if rules are not yet defined + if (ruleKeys.stream().noneMatch(rule -> SECURITY_REPOSITORY_KEY.equals(rule.repository()))) { + ruleKeys.addAll(getSecurityRuleKeys()); + } + if (ruleKeys.stream().noneMatch(rule -> DBD_REPOSITORY_KEY.equals(rule.repository()))) { + ruleKeys.addAll(getDataflowBugDetectionRuleKeys()); + } - getSecurityRuleKeys().forEach(key -> sonarWay.activateRule(key.repository(), key.rule())); - getDataflowBugDetectionRuleKeys().forEach(key -> sonarWay.activateRule(key.repository(), key.rule())); + ruleKeys.forEach(ruleKey -> sonarWay.activateRule(ruleKey.repository(), ruleKey.rule())); sonarWay.done(); } - static Set ruleKeys() { - return BuiltInQualityProfileJsonLoader.loadActiveKeysFromJsonProfile(SONAR_WAY_PATH); + static Set sonarJavaSonarWayRuleKeys() { + return BuiltInQualityProfileJsonLoader.loadActiveKeysFromJsonProfile(SONAR_WAY_PATH).stream() + .map(rule -> RuleKey.of(CheckList.REPOSITORY_KEY, rule)) + .collect(Collectors.toSet()); } @VisibleForTesting diff --git a/sonar-java-plugin/src/main/resources/static/documentation.md b/sonar-java-plugin/src/main/resources/static/documentation.md index 913dccbf9d5..a4524141a7b 100644 --- a/sonar-java-plugin/src/main/resources/static/documentation.md +++ b/sonar-java-plugin/src/main/resources/static/documentation.md @@ -142,6 +142,21 @@ The tutorial [Writing Custom Java Rules 101](https://redirect.sonarsource.com/do ### API changes +#### **7.25** + +Update custom rules registration API `CheckRegistrar.RegistrarContext` to allow: +* Registration of JavaCheck instances (previously only classes were supported) + `registerMainChecks(String repositoryKey, Collection javaCheckClassesAndInstances)` + `registerTestChecks(String repositoryKey, Collection javaCheckClassesAndInstances)` +* Registration of a JavaCheck that has no `@Rule` annotation because it does not target one but several rules. + `registerMainSharedCheck(JavaCheck check, Collection ruleKeys)` + `registerTestSharedCheck(JavaCheck check, Collection ruleKeys)` +* Deprecate useless getters from the RegistrarContext class and add `TestCheckRegistrarContext` for test purpose. + +Add custom rules registration API `ProfileRegistrar.RegistrarContext` to allow: +* Registration of rules in the "Sonar Way" builtin default quality profile for the Java language. + `registerDefaultQualityProfileRules(Collection ruleKeys)` + #### **7.19** All the API changes are related to the support of the preview feature of Java 19/20. These new types and methods are introduced as "deprecated" and will be marked out of deprecation with the introduction of the final features in the JDK. diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java index 5966ac75fad..2e3598d4728 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java @@ -40,6 +40,7 @@ import org.sonar.api.batch.fs.internal.DefaultFileSystem; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.batch.fs.internal.TestInputFileBuilder; +import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.batch.rule.Checks; import org.sonar.api.batch.rule.internal.ActiveRulesBuilder; @@ -112,7 +113,7 @@ void test_toString() throws IOException { @Test void test_issues_creation_on_main_file() throws IOException { - testIssueCreation(InputFile.Type.MAIN, 19); + testIssueCreation(InputFile.Type.MAIN, 18); } @Test @@ -132,7 +133,7 @@ private void testIssueCreation(InputFile.Type onType, int expectedIssues) throws jss.execute(context); // argument 119 refers to the comment on line #119 in this file - verify(noSonarFilter, times(1)).noSonarInFile(fs.inputFiles().iterator().next(), Collections.singleton(119)); + verify(noSonarFilter, times(1)).noSonarInFile(fs.inputFiles().iterator().next(), Collections.singleton(120)); verify(sonarComponents, times(expectedIssues)).reportIssue(any(AnalyzerMessage.class)); settings.setProperty(JavaVersion.SOURCE_VERSION, "wrongFormat"); @@ -165,7 +166,8 @@ private static SonarComponents createSonarComponentsMock(SensorContextTester con FileLinesContext fileLinesContext = mock(FileLinesContext.class); FileLinesContextFactory fileLinesContextFactory = mock(FileLinesContextFactory.class); when(fileLinesContextFactory.createFor(any(InputFile.class))).thenReturn(fileLinesContext); - SonarComponents sonarComponents = spy(new SonarComponents(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory)); + SonarComponents sonarComponents = spy(new SonarComponents(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, + checkFactory, mock(ActiveRules.class))); sonarComponents.setSensorContext(contextTester); BadMethodNameCheck check = new BadMethodNameCheck(); @@ -432,15 +434,6 @@ void filter_checks_when_autoscan_true() throws IOException { ); } - @Test - void extract_rule_key() { - @org.sonar.check.Rule(key = "123") - class MyRule implements JavaCheck {} - class MyRuleWithoutKey implements JavaCheck {} - assertThat(JavaSensor.getKeyFromCheck(new MyRule())).isEqualTo("123"); - assertThat(JavaSensor.getKeyFromCheck(new MyRuleWithoutKey())).isEmpty(); - } - private SensorContextTester analyzeTwoFilesWithIssues(MapSettings settings) throws IOException { SensorContextTester context = SensorContextTester.create(new File("src/test/files").getAbsoluteFile()) .setSettings(settings) @@ -479,7 +472,7 @@ private SensorContextTester analyzeTwoFilesWithIssues(MapSettings settings) thro CheckFactory checkFactory = new CheckFactory(activeRulesBuilder.build()); SonarComponents components = new SonarComponents(fileLinesContextFactory, fs, - javaClasspath, javaTestClasspath, checkFactory, checkRegistrars, null); + javaClasspath, javaTestClasspath, checkFactory, context.activeRules(), checkRegistrars, null); JavaSensor jss = new JavaSensor(components, fs, resourceLocator, context.config(), mock(NoSonarFilter.class), null); jss.execute(context); diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSonarWayProfileTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSonarWayProfileTest.java index 3c0c0497f28..bdfd3370b32 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSonarWayProfileTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSonarWayProfileTest.java @@ -30,6 +30,8 @@ import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition; import org.sonar.api.testfixtures.log.LogTesterJUnit5; import org.sonar.api.utils.ValidationMessages; +import org.sonar.java.SonarComponents; +import org.sonar.plugins.java.api.ProfileRegistrar; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.plugins.java.JavaSonarWayProfile.DBD_RULES_CLASS_NAME; @@ -39,11 +41,21 @@ class JavaSonarWayProfileTest { @RegisterExtension public LogTesterJUnit5 logTester = new LogTesterJUnit5().setLevel(Level.DEBUG); + SonarComponents sonarComponents = new SonarComponents(null,null,null,null,null,null); + @Test void should_create_sonar_way_profile() { ValidationMessages validation = ValidationMessages.create(); - JavaSonarWayProfile profileDef = new JavaSonarWayProfile(); + ProfileRegistrar fooCustomRules = registrarContext -> registrarContext.registerDefaultQualityProfileRules(List.of( + RuleKey.of("javasecurity", "S6549"), + RuleKey.of("javasecurity", "S6287"))); + ProfileRegistrar barCustomRules = registrarContext -> registrarContext.registerDefaultQualityProfileRules(List.of( + RuleKey.of("javabugs", "S6466"))); + + JavaSonarWayProfile profileDef = new JavaSonarWayProfile(new ProfileRegistrar[] { + fooCustomRules, + barCustomRules}); BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context(); profileDef.define(context); BuiltInQualityProfilesDefinition.BuiltInQualityProfile profile = context.profile("java", "Sonar way"); @@ -52,13 +64,19 @@ void should_create_sonar_way_profile() { assertThat(activeRules.stream().filter(r -> r.repoKey().equals("common-java"))).isEmpty(); assertThat(activeRules).as("Expected number of rules in profile").hasSizeGreaterThanOrEqualTo(268); assertThat(profile.name()).isEqualTo("Sonar way"); - Set keys = new HashSet<>(); + Set keys = new HashSet<>(); for (BuiltInQualityProfilesDefinition.BuiltInActiveRule activeRule : activeRules) { - keys.add(activeRule.ruleKey()); + keys.add(RuleKey.of(activeRule.repoKey(), activeRule.ruleKey())); } - //We no longer store active rules with legacy keys, only RSPEC keys are used. - assertThat(keys).doesNotContain("S00116") - .contains("S116"); + // We no longer store active rules with legacy keys, only RSPEC keys are used. + assertThat(keys) + .doesNotContain(RuleKey.of("java", "S00116")) + .contains(RuleKey.of("java", "S116")) + .doesNotContain(RuleKey.of("java", "S6549")) + .doesNotContain(RuleKey.of("javasecurity", "S116")) + .contains(RuleKey.of("javasecurity", "S6549")) + .contains(RuleKey.of("javasecurity", "S6287")) + .contains(RuleKey.of("javabugs", "S6466")); assertThat(validation.hasErrors()).isFalse(); // Check that we use severity from the read rule and not default one. @@ -67,7 +85,7 @@ void should_create_sonar_way_profile() { @Test void should_activate_hotspots_when_supported() { - JavaSonarWayProfile profileDef = new JavaSonarWayProfile(); + JavaSonarWayProfile profileDef = new JavaSonarWayProfile(null); BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context(); profileDef.define(context); BuiltInQualityProfilesDefinition.BuiltInQualityProfile profile = context.profile("java", "Sonar way");