diff --git a/pom.xml b/pom.xml index 3fd1a140b57..fd0fa8f0522 100644 --- a/pom.xml +++ b/pom.xml @@ -280,6 +280,11 @@ commons-lang3 3.20.0 + + org.apache.commons + commons-csv + 1.14.1 + org.springframework spring-expression diff --git a/sonar-java-plugin/pom.xml b/sonar-java-plugin/pom.xml index e8818a56508..7640a1028ab 100644 --- a/sonar-java-plugin/pom.xml +++ b/sonar-java-plugin/pom.xml @@ -137,6 +137,11 @@ ${project.version} test + + org.apache.commons + commons-csv + test + diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/BuiltInJavaQualityProfile.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/BuiltInJavaQualityProfile.java new file mode 100644 index 00000000000..2858e5c702d --- /dev/null +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/BuiltInJavaQualityProfile.java @@ -0,0 +1,117 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.plugins.java; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +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.GeneratedCheckList; +import org.sonar.java.annotations.VisibleForTesting; +import org.sonar.plugins.java.api.ProfileRegistrar; +import org.sonarsource.analyzer.commons.BuiltInQualityProfileJsonLoader; + + +/** + * Defines a Java quality profile including rules from sonar-java, Java SE, DBD and Security. + */ +abstract class BuiltInJavaQualityProfile implements BuiltInQualityProfilesDefinition { + private static final Logger LOG = LoggerFactory.getLogger(BuiltInJavaQualityProfile.class); + + static final String SECURITY_RULES_CLASS_NAME = "com.sonar.plugins.security.api.JavaRules"; + static final String DBD_RULES_CLASS_NAME = "com.sonarsource.plugins.dbd.api.JavaRules"; + static final String SECURITY_RULE_KEYS_METHOD_NAME = "getSecurityRuleKeys"; + static final String GET_REPOSITORY_KEY = "getRepositoryKey"; + static final String SECURITY_REPOSITORY_KEY = "javasecurity"; + + + protected final ProfileRegistrar[] profileRegistrars; + + BuiltInJavaQualityProfile(@Nullable ProfileRegistrar[] profileRegistrars) { + this.profileRegistrars = profileRegistrars; + } + + abstract String getProfileName(); + + abstract String getPathToJsonProfile(); + + abstract boolean isDefault(); + + @Override + public void define(Context context) { + // Create a new profile + BuiltInQualityProfilesDefinition.NewBuiltInQualityProfile profile = context.createBuiltInQualityProfile(getProfileName(), Java.KEY); + // Load rules from local JSON + Set ruleKeys = registerRulesFromJson(getPathToJsonProfile(), profileRegistrars); + + // FIXME as part of SONARJAVA-6207 + // 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()); + } + + ruleKeys.forEach(ruleKey -> profile.activateRule(ruleKey.repository(), ruleKey.rule())); + profile.setDefault(isDefault()); + profile.done(); + } + + static Set registerRulesFromJson(String pathToJsonProfile, @Nullable ProfileRegistrar[] profileRegistrars) { + Set ruleKeys = new HashSet<>(loadRuleKeys(pathToJsonProfile)); + if (profileRegistrars != null) { + for (ProfileRegistrar profileRegistrar : profileRegistrars) { + profileRegistrar.register(ruleKeys::addAll); + } + } + + return ruleKeys; + } + + static Set loadRuleKeys(final String pathToJsonProfile) { + return BuiltInQualityProfileJsonLoader.loadActiveKeysFromJsonProfile(pathToJsonProfile).stream() + .map(rule -> RuleKey.of(GeneratedCheckList.REPOSITORY_KEY, rule)) + .collect(Collectors.toSet()); + } + + @VisibleForTesting + Set getSecurityRuleKeys() { + return getExternalRuleKeys(SECURITY_RULES_CLASS_NAME, SECURITY_RULE_KEYS_METHOD_NAME, "security"); + } + + @VisibleForTesting + Set getExternalRuleKeys(String className, String ruleKeysMethod, String rulesCategory) { + try { + Class javaRulesClass = Class.forName(className); + Method getRuleKeysMethod = javaRulesClass.getMethod(ruleKeysMethod); + Set ruleKeys = (Set) getRuleKeysMethod.invoke(null); + Method getRepositoryKeyMethod = javaRulesClass.getMethod(GET_REPOSITORY_KEY); + String repositoryKey = (String) getRepositoryKeyMethod.invoke(null); + return ruleKeys.stream().map(k -> RuleKey.of(repositoryKey, k)).collect(Collectors.toSet()); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + LOG.debug(String.format("[%s], no %s rules added to %s java profile: %s", e.getClass().getSimpleName(), rulesCategory, getProfileName(), e.getMessage())); + } + return new HashSet<>(); + } +} diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaAgenticAIProfile.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaAgenticAIProfile.java new file mode 100644 index 00000000000..1831bc0b68d --- /dev/null +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaAgenticAIProfile.java @@ -0,0 +1,49 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.plugins.java; + +import javax.annotation.Nullable; +import org.sonar.plugins.java.api.ProfileRegistrar; +import org.sonarsource.api.sonarlint.SonarLintSide; + +@SonarLintSide +public class JavaAgenticAIProfile extends BuiltInJavaQualityProfile { + static final String PROFILE_NAME = "Sonar agentic AI"; + + public JavaAgenticAIProfile() { + this(null); + } + + public JavaAgenticAIProfile(@Nullable ProfileRegistrar[] profileRegistrars) { + super(profileRegistrars); + } + + @Override + String getProfileName() { + return PROFILE_NAME; + } + + @Override + String getPathToJsonProfile() { + return "/org/sonar/l10n/java/rules/java/Sonar_agentic_ai_profile.json"; + } + + @Override + boolean isDefault() { + return false; + } +} diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java index c1f64a5613c..3de169b4f4a 100644 --- a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java @@ -62,6 +62,7 @@ public void define(Context context) { list.addAll(SurefireExtensions.getExtensions()); list.add(DroppedPropertiesSensor.class); list.add(JavaSonarWayProfile.class); + list.add(JavaAgenticAIProfile.class); list.add(ClasspathForMain.class); ExternalReportExtensions.define(context); 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 c2b47e553de..f8792aadce1 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 @@ -16,42 +16,19 @@ */ package org.sonar.plugins.java; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -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.GeneratedCheckList; -import org.sonar.java.annotations.VisibleForTesting; import org.sonar.plugins.java.api.ProfileRegistrar; -import org.sonarsource.analyzer.commons.BuiltInQualityProfileJsonLoader; import org.sonarsource.api.sonarlint.SonarLintSide; /** - * define built-in profile + * Define the default Sonar way profile. */ @SonarLintSide -public class JavaSonarWayProfile implements BuiltInQualityProfilesDefinition { - - private static final Logger LOG = LoggerFactory.getLogger(JavaSonarWayProfile.class); - - static final String SECURITY_RULES_CLASS_NAME = "com.sonar.plugins.security.api.JavaRules"; - static final String DBD_RULES_CLASS_NAME = "com.sonarsource.plugins.dbd.api.JavaRules"; - 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"; - +public class JavaSonarWayProfile extends BuiltInJavaQualityProfile { static final String SONAR_WAY_PATH = "/org/sonar/l10n/java/rules/java/Sonar_way_profile.json"; - private final ProfileRegistrar[] profileRegistrars; - /** * Constructor used by Pico container (SC) when no ProfileRegistrar are available */ @@ -60,63 +37,25 @@ public JavaSonarWayProfile() { } public JavaSonarWayProfile(@Nullable ProfileRegistrar[] profileRegistrars) { - this.profileRegistrars = profileRegistrars; + super(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); - } - } - - // 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()); - } - - ruleKeys.forEach(ruleKey -> sonarWay.activateRule(ruleKey.repository(), ruleKey.rule())); - sonarWay.done(); + String getProfileName() { + return "Sonar way"; } - static Set sonarJavaSonarWayRuleKeys() { - return BuiltInQualityProfileJsonLoader.loadActiveKeysFromJsonProfile(SONAR_WAY_PATH).stream() - .map(rule -> RuleKey.of(GeneratedCheckList.REPOSITORY_KEY, rule)) - .collect(Collectors.toSet()); - } - - @VisibleForTesting - static Set getSecurityRuleKeys() { - return getExternalRuleKeys(SECURITY_RULES_CLASS_NAME, SECURITY_RULE_KEYS_METHOD_NAME, "security"); + @Override + String getPathToJsonProfile() { + return SONAR_WAY_PATH; } - @VisibleForTesting - static Set getDataflowBugDetectionRuleKeys() { - return getExternalRuleKeys(DBD_RULES_CLASS_NAME, DBD_RULE_KEYS_METHOD_NAME, "dataflow bug detection"); + @Override + boolean isDefault() { + return true; } - @SuppressWarnings("unchecked") - @VisibleForTesting - static Set getExternalRuleKeys(String className, String ruleKeysMethod, String rulesCategory) { - try { - Class javaRulesClass = Class.forName(className); - Method getRuleKeysMethod = javaRulesClass.getMethod(ruleKeysMethod); - Set ruleKeys = (Set) getRuleKeysMethod.invoke(null); - Method getRepositoryKeyMethod = javaRulesClass.getMethod(GET_REPOSITORY_KEY); - String repositoryKey = (String) getRepositoryKeyMethod.invoke(null); - return ruleKeys.stream().map(k -> RuleKey.of(repositoryKey, k)).collect(Collectors.toSet()); - } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - LOG.debug(String.format("[%s], no %s rules added to Sonar way java profile: %s", e.getClass().getSimpleName(), rulesCategory, e.getMessage())); - } - return new HashSet<>(); + static Set sonarJavaSonarWayRuleKeys() { + return loadRuleKeys(SONAR_WAY_PATH); } } diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_ai_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_ai_profile.json new file mode 100644 index 00000000000..8938ea8aad5 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_ai_profile.json @@ -0,0 +1,472 @@ +{ + "name": "Sonar Agentic AI", + "ruleKeys": [ + "S106", + "S107", + "S108", + "S112", + "S119", + "S120", + "S125", + "S127", + "S128", + "S131", + "S135", + "S899", + "S1065", + "S1068", + "S1111", + "S1113", + "S1116", + "S1117", + "S1118", + "S1119", + "S1121", + "S1123", + "S1125", + "S1126", + "S1128", + "S1130", + "S1133", + "S1134", + "S1135", + "S1141", + "S1143", + "S1144", + "S1149", + "S1150", + "S1153", + "S1155", + "S1157", + "S1158", + "S1161", + "S1163", + "S1165", + "S1168", + "S1170", + "S1171", + "S1172", + "S1174", + "S1175", + "S1181", + "S1182", + "S1185", + "S1186", + "S1190", + "S1191", + "S1192", + "S1193", + "S1199", + "S1201", + "S1206", + "S1210", + "S1214", + "S1215", + "S1217", + "S1219", + "S1220", + "S1221", + "S1223", + "S1226", + "S1264", + "S1301", + "S1313", + "S1317", + "S1319", + "S1444", + "S1450", + "S1452", + "S1479", + "S1481", + "S1488", + "S1596", + "S1598", + "S1602", + "S1604", + "S1607", + "S1612", + "S1640", + "S1643", + "S1656", + "S1700", + "S1751", + "S1764", + "S1844", + "S1845", + "S1849", + "S1854", + "S1858", + "S1860", + "S1862", + "S1871", + "S1872", + "S1874", + "S1905", + "S1940", + "S1948", + "S1989", + "S1994", + "S2053", + "S2055", + "S2060", + "S2061", + "S2062", + "S2065", + "S2066", + "S2068", + "S2077", + "S2092", + "S2093", + "S2094", + "S2097", + "S2109", + "S2110", + "S2111", + "S2112", + "S2114", + "S2115", + "S2116", + "S2118", + "S2119", + "S2121", + "S2122", + "S2123", + "S2127", + "S2129", + "S2130", + "S2133", + "S2134", + "S2139", + "S2140", + "S2142", + "S2147", + "S2151", + "S2153", + "S2154", + "S2157", + "S2159", + "S2160", + "S2166", + "S2167", + "S2168", + "S2175", + "S2176", + "S2177", + "S2178", + "S2183", + "S2184", + "S2185", + "S2186", + "S2187", + "S2188", + "S2200", + "S2201", + "S2204", + "S2209", + "S2225", + "S2226", + "S2229", + "S2230", + "S2232", + "S2234", + "S2235", + "S2236", + "S2245", + "S2251", + "S2252", + "S2254", + "S2257", + "S2272", + "S2273", + "S2274", + "S2275", + "S2276", + "S2293", + "S2326", + "S2386", + "S2387", + "S2388", + "S2390", + "S2437", + "S2438", + "S2440", + "S2441", + "S2442", + "S2445", + "S2446", + "S2447", + "S2479", + "S2612", + "S2629", + "S2638", + "S2639", + "S2674", + "S2675", + "S2676", + "S2677", + "S2681", + "S2692", + "S2695", + "S2696", + "S2699", + "S2718", + "S2737", + "S2757", + "S2761", + "S2786", + "S2789", + "S2864", + "S2885", + "S2886", + "S2924", + "S2925", + "S2970", + "S2975", + "S3008", + "S3010", + "S3011", + "S3012", + "S3014", + "S3020", + "S3024", + "S3033", + "S3034", + "S3038", + "S3039", + "S3042", + "S3046", + "S3051", + "S3063", + "S3064", + "S3066", + "S3067", + "S3077", + "S3078", + "S3252", + "S3305", + "S3329", + "S3330", + "S3346", + "S3358", + "S3398", + "S3400", + "S3415", + "S3416", + "S3436", + "S3457", + "S3551", + "S3577", + "S3599", + "S3626", + "S3631", + "S3740", + "S3751", + "S3752", + "S3753", + "S3776", + "S3864", + "S3878", + "S3923", + "S3972", + "S3973", + "S3981", + "S3984", + "S3985", + "S3986", + "S4030", + "S4032", + "S4034", + "S4036", + "S4042", + "S4065", + "S4087", + "S4143", + "S4144", + "S4201", + "S4274", + "S4275", + "S4276", + "S4347", + "S4348", + "S4349", + "S4351", + "S4423", + "S4425", + "S4426", + "S4433", + "S4434", + "S4454", + "S4488", + "S4502", + "S4507", + "S4512", + "S4517", + "S4524", + "S4544", + "S4601", + "S4602", + "S4635", + "S4682", + "S4684", + "S4738", + "S4790", + "S4830", + "S4970", + "S4973", + "S5042", + "S5122", + "S5164", + "S5247", + "S5301", + "S5320", + "S5322", + "S5324", + "S5332", + "S5344", + "S5361", + "S5443", + "S5445", + "S5527", + "S5542", + "S5547", + "S5659", + "S5679", + "S5689", + "S5693", + "S5738", + "S5776", + "S5777", + "S5778", + "S5779", + "S5783", + "S5785", + "S5790", + "S5803", + "S5804", + "S5808", + "S5810", + "S5826", + "S5831", + "S5833", + "S5838", + "S5842", + "S5843", + "S5845", + "S5846", + "S5850", + "S5852", + "S5855", + "S5856", + "S5863", + "S5866", + "S5868", + "S5876", + "S5917", + "S5960", + "S5967", + "S5969", + "S5994", + "S5996", + "S5998", + "S6001", + "S6002", + "S6068", + "S6070", + "S6103", + "S6104", + "S6126", + "S6201", + "S6202", + "S6203", + "S6204", + "S6205", + "S6206", + "S6207", + "S6208", + "S6209", + "S6216", + "S6218", + "S6243", + "S6262", + "S6263", + "S6288", + "S6293", + "S6301", + "S6326", + "S6331", + "S6353", + "S6355", + "S6362", + "S6363", + "S6395", + "S6396", + "S6397", + "S6418", + "S6432", + "S6437", + "S6485", + "S6539", + "S6541", + "S6548", + "S6806", + "S6809", + "S6810", + "S6814", + "S6816", + "S6817", + "S6818", + "S6831", + "S6838", + "S6856", + "S6857", + "S6862", + "S6863", + "S6876", + "S6877", + "S6878", + "S6880", + "S6881", + "S6885", + "S6889", + "S6901", + "S6905", + "S6906", + "S6909", + "S6913", + "S6915", + "S6916", + "S7158", + "S7177", + "S7178", + "S7179", + "S7180", + "S7183", + "S7184", + "S7185", + "S7186", + "S7190", + "S7409", + "S7435", + "S7466", + "S7467", + "S7474", + "S7475", + "S7476", + "S7477", + "S7478", + "S7479", + "S7481", + "S7482", + "S7629", + "S8346", + "S8432", + "S8433", + "S8444", + "S8445", + "S8446", + "S8447", + "S8450", + "S8465", + "S8469" + ] +} \ No newline at end of file diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java new file mode 100644 index 00000000000..2209bd62e86 --- /dev/null +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java @@ -0,0 +1,202 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.plugins.java; + + +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.csv.CSVFormat; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +class JavaAgenticWayProfileTest { + + private static final Path RULE_DESCRIPTION_DIRECTORY = Path.of("src", "main", "resources", "org", "sonar", "l10n", "java", "rules", "java"); + + @Test + void profile_is_registered_as_expected() { + JavaAgenticAIProfile profile = new JavaAgenticAIProfile(); + BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context(); + profile.define(context); + + Map> profilesPerLanguages = context.profilesByLanguageAndName(); + assertThat(profilesPerLanguages).containsOnlyKeys("java"); + assertThat(profilesPerLanguages.get("java")).containsOnlyKeys("Sonar agentic AI"); + + BuiltInQualityProfilesDefinition.BuiltInQualityProfile actualProfile = profilesPerLanguages.get("java").get("Sonar agentic AI"); + assertThat(actualProfile.isDefault()).isFalse(); + assertThat(actualProfile.rules()) + .hasSize(467) + .extracting(BuiltInQualityProfilesDefinition.BuiltInActiveRule::ruleKey) + .doesNotContainAnyElementsOf(List.of( + "S101", + "S110", + "S114", + "S115", + "S116", + "S117", + "S1066", + "S1075", + "S1104", + "S1110", + "S1124", + "S1195", + "S1197", + "S1611", + "S1659", + "S1710", + "S4719", + "S4838", + "S4925", + "S4929", + "S4968", + "S4977", + "S5261", + "S5329", + "S5411", + "S5413", + "S5663", + "S5664", + "S5665", + "S5669", + "S5786", + "S5841", + "S5853", + "S5854", + "S5857", + "S5860", + "S5869", + "S5958", + "S5961", + "S5973", + "S5976", + "S5993", + "S6019", + "S6035", + "S6213", + "S6217", + "S6219", + "S6241", + "S6242", + "S6244", + "S6246", + "S6804", + "S6813", + "S6829", + "S6830", + "S6832", + "S6833", + "S6837", + "S6912", + "S8491" + )); + } + + + @Test + @Disabled("A utility method to generate a quality profile from a CSV file") + void generate_ai_quality_profile() throws IOException { + Path generatedQualityProfile = generate( + Path.of("Path to your CSV input file relative to sonar-java-plugin"), + RULE_DESCRIPTION_DIRECTORY.resolve("Sonar_agentic_ai_profile.json"), + JavaAgenticAIProfile.PROFILE_NAME + ); + Assertions.fail(String.format("The generated quality profile was written to %s".formatted(generatedQualityProfile))); + } + + static Path generate(Path input, Path output, String profileName) throws IOException { + CSVFormat csvFormat = CSVFormat.DEFAULT.builder() + .setHeader() + .setSkipHeaderRecord(true) + .get(); + + Set keysOfImplementedRules = getImplementedRuleKeys(); + + String ruleKeys; + try (FileReader in = new FileReader(input.toFile(), StandardCharsets.UTF_8)) { + ruleKeys = csvFormat.parse(in).stream() + // Filter rules that have not been classified as AI and Sonar Way + .filter(ruleRecord -> "AI and Sonar way".equalsIgnoreCase(ruleRecord.get("classification_status"))) + // Recover keys + .map(ruleRecord -> ruleRecord.get("ruleid")) + // Filter out keys that do not have a sonar-java implementation + .filter(keysOfImplementedRules::contains) + // Sort keys + .sorted(JavaAgenticWayProfileTest::compareRuleKeys) + // Surround with double quotes for output in JSON document + .map(" \"%s\""::formatted) + // Comma and line separate rule keys + .collect(Collectors.joining(",%s".formatted(System.lineSeparator()))); + } + try ( + FileWriter out = new FileWriter(output.toFile(), StandardCharsets.UTF_8); + BufferedWriter writer = new BufferedWriter(out) + ) { + writer.write("{"); + writer.newLine(); + writer.write(" \"name\": \"%s\",".formatted(profileName)); + writer.newLine(); + writer.write(" \"ruleKeys\": ["); + writer.newLine(); + writer.write(ruleKeys); + writer.newLine(); + writer.write(" ]"); + writer.newLine(); + writer.write("}"); + } + return output; + + } + + private static Set getImplementedRuleKeys() throws IOException { + if (!Files.isDirectory(RULE_DESCRIPTION_DIRECTORY)) { + throw new IllegalStateException("Could not find path to %s".formatted(RULE_DESCRIPTION_DIRECTORY)); + } + return Files.list(RULE_DESCRIPTION_DIRECTORY) + .filter(path -> !path.toString().endsWith("_profile.json")) + .map(path -> { + String fileName = path.getFileName().toString(); + return fileName.substring(0, fileName.lastIndexOf('.')); + }) + .collect(Collectors.toSet()); + } + + private static Integer getSortingKey(String ruleKey) { + try { + return Integer.parseInt(ruleKey.substring(1)); + } catch (NumberFormatException ignored) { + return Integer.MIN_VALUE; + } + } + + private static int compareRuleKeys(String first, String second) { + return getSortingKey(first) - getSortingKey(second); + } +} diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java index eb700626422..06031c47465 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java @@ -51,7 +51,7 @@ void sonarqube_9_9_extensions() { Plugin.Context context = new Plugin.Context(sqCommunity); javaPlugin.define(context); assertThat(context.getExtensions()) - .hasSize(36) + .hasSize(37) .doesNotContain(Jasper.class); } @@ -61,7 +61,7 @@ void sonarqube_9_9_commercial_extensions() { Plugin.Context context = new Plugin.Context(sqEnterprise); javaPlugin.define(context); assertThat(context.getExtensions()) - .hasSize(37) + .hasSize(38) .contains(Jasper.class); } 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 25a1c717e18..b9c23a450e7 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 @@ -62,6 +62,7 @@ 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"); + assertThat(profile.isDefault()).isTrue(); Set keys = new HashSet<>(); for (BuiltInQualityProfilesDefinition.BuiltInActiveRule activeRule : activeRules) { keys.add(RuleKey.of(activeRule.repoKey(), activeRule.ruleKey())); @@ -95,40 +96,29 @@ void should_activate_hotspots_when_supported() { void should_contains_security_rules_if_present() { // no security rules available com.sonar.plugins.security.api.JavaRules.ruleKeys = new HashSet<>(); - assertThat(JavaSonarWayProfile.getSecurityRuleKeys()).isEmpty(); + assertThat(new JavaSonarWayProfile().getSecurityRuleKeys()).isEmpty(); // one security rule available com.sonar.plugins.security.api.JavaRules.ruleKeys = new HashSet<>(Arrays.asList("S3649")); - assertThat(JavaSonarWayProfile.getSecurityRuleKeys()).containsOnly(RuleKey.of("security-repo-key", "S3649")); - } - - @Test - void should_contains_dataflow_bug_detection_rules_if_present() { - // no dataflow bug detection rules available - com.sonarsource.plugins.dbd.api.JavaRules.ruleKeys = new HashSet<>(); - assertThat(JavaSonarWayProfile.getDataflowBugDetectionRuleKeys()).isEmpty(); - - // one dataflow bug detection rule available - com.sonarsource.plugins.dbd.api.JavaRules.ruleKeys = new HashSet<>(Arrays.asList("S6322")); - assertThat(JavaSonarWayProfile.getDataflowBugDetectionRuleKeys()).containsOnly(RuleKey.of("dbd-repo-key", "S6322")); + assertThat(new JavaSonarWayProfile().getSecurityRuleKeys()).containsOnly(RuleKey.of("security-repo-key", "S3649")); } @Test void external_rule_keys_missing_class() { - JavaSonarWayProfile.getExternalRuleKeys("silly.name", "getDataflowBugDetectionRuleKeys", "ruleCategory"); + new JavaSonarWayProfile().getExternalRuleKeys("silly.name", "getDataflowBugDetectionRuleKeys", "ruleCategory"); assertThat(logTester.logs(Level.DEBUG)).containsExactly("[ClassNotFoundException], no ruleCategory rules added to Sonar way java profile: silly.name"); } @Test void external_rule_keys_missing_method() { - JavaSonarWayProfile.getExternalRuleKeys(DBD_RULES_CLASS_NAME, "nonExistingRuleKeysMethod", "ruleCategory"); + new JavaSonarWayProfile().getExternalRuleKeys(DBD_RULES_CLASS_NAME, "nonExistingRuleKeysMethod", "ruleCategory"); assertThat(logTester.logs(Level.DEBUG)) .containsExactly("[NoSuchMethodException], no ruleCategory rules added to Sonar way java profile: com.sonarsource.plugins.dbd.api.JavaRules.nonExistingRuleKeysMethod()"); } @Test void external_rule_keys_method_throws_exception() { - JavaSonarWayProfile.getExternalRuleKeys(DBD_RULES_CLASS_NAME, "methodThrowingException", "ruleCategory"); + new JavaSonarWayProfile().getExternalRuleKeys(DBD_RULES_CLASS_NAME, "methodThrowingException", "ruleCategory"); assertThat(logTester.logs(Level.DEBUG)).containsExactly("[InvocationTargetException], no ruleCategory rules added to Sonar way java profile: null"); } }