From d0e3bcc38df9b467cba99a130f8246bd145eb170 Mon Sep 17 00:00:00 2001 From: Maksim Grebeniuk Date: Wed, 27 Nov 2024 12:24:38 +0100 Subject: [PATCH] SONARPY-2379 Update the custom rules in the sonar-python repository to match LAYC format --- docs/python-custom-rule-examples/pom.xml | 28 +++++++-- .../python/CustomPythonRuleRepository.java | 62 ++++++------------- .../org/sonar/samples/python/RulesList.java | 42 +++++++++++++ .../checks/CustomPythonSubscriptionCheck.java | 7 +-- .../checks/CustomPythonVisitorCheck.java | 7 +-- .../python/rules/python/subscription.json | 13 ++++ .../l10n/python/rules/python/visitor.json | 13 ++++ .../CustomPythonRuleRepositoryTest.java | 32 +++------- .../python/CustomPythonRulesPluginTest.java | 26 +++++++- 9 files changed, 145 insertions(+), 85 deletions(-) create mode 100644 docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/RulesList.java create mode 100644 docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/subscription.json create mode 100644 docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/visitor.json diff --git a/docs/python-custom-rule-examples/pom.xml b/docs/python-custom-rule-examples/pom.xml index ba061a7be9..239f371281 100644 --- a/docs/python-custom-rule-examples/pom.xml +++ b/docs/python-custom-rule-examples/pom.xml @@ -16,13 +16,13 @@ Python custom rule examples for SonarQube - 3.15.0.9787 + 4.24.0.18631 - org.sonarsource.sonarqube + org.sonarsource.api.plugin sonar-plugin-api - 7.9 + 10.12.0.2522 provided @@ -32,12 +32,28 @@ ${sonar.python.version} provided + + org.slf4j + slf4j-api + provided + org.sonarsource.python python-checks-testkit ${sonar.python.version} test + + org.sonarsource.sonarqube + sonar-plugin-api-impl + 10.7.0.96327 + test + + + org.sonarsource.api.plugin + sonar-plugin-api-test-fixtures + test + junit junit @@ -59,6 +75,8 @@ 1.21.0.505 true + python-custom + Python Custom Rules org.sonar.samples.python.CustomPythonRulesPlugin python:${sonar.python.version} true @@ -70,8 +88,8 @@ maven-compiler-plugin 3.7.0 - 1.8 - 1.8 + 17 + 17 diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/CustomPythonRuleRepository.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/CustomPythonRuleRepository.java index ee8d080fcf..1a5b781abd 100644 --- a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/CustomPythonRuleRepository.java +++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/CustomPythonRuleRepository.java @@ -4,35 +4,30 @@ */ package org.sonar.samples.python; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; +import org.sonar.api.SonarRuntime; import org.sonar.api.server.rule.RulesDefinition; -import org.sonar.api.server.rule.RulesDefinitionAnnotationLoader; import org.sonar.plugins.python.api.PythonCustomRuleRepository; -import org.sonar.samples.python.checks.CustomPythonSubscriptionCheck; -import org.sonar.samples.python.checks.CustomPythonVisitorCheck; +import org.sonarsource.analyzer.commons.RuleMetadataLoader; public class CustomPythonRuleRepository implements RulesDefinition, PythonCustomRuleRepository { + public static final String RESOURCE_BASE_PATH = "/org/sonar/l10n/python/rules/python/"; + public static final String REPOSITORY_KEY = "python-custom-rules"; + public static final String REPOSITORY_NAME = "MyCompany Custom Repository"; + + private final SonarRuntime runtime; + + public CustomPythonRuleRepository(SonarRuntime runtime) { + this.runtime = runtime; + } + @Override public void define(Context context) { - NewRepository repository = context.createRepository(repositoryKey(), "py").setName("My custom repo"); - new RulesDefinitionAnnotationLoader().load(repository, checkClasses().toArray(new Class[] {})); - Map remediationCosts = new HashMap<>(); - remediationCosts.put(CustomPythonVisitorCheck.RULE_KEY, "5min"); - remediationCosts.put(CustomPythonSubscriptionCheck.RULE_KEY, "10min"); - repository.rules().forEach(rule -> rule.setDebtRemediationFunction( - rule.debtRemediationFunctions().constantPerIssue(remediationCosts.get(rule.key())))); - - // Optionally override html description from annotation with content from html files - repository.rules().forEach(rule -> rule.setHtmlDescription(loadResource("/org/sonar/l10n/python/rules/python/" + rule.key() + ".html"))); + NewRepository repository = context.createRepository(REPOSITORY_KEY, "java").setName(REPOSITORY_NAME); + RuleMetadataLoader ruleMetadataLoader = new RuleMetadataLoader(RESOURCE_BASE_PATH, runtime); + ruleMetadataLoader.addRulesByAnnotatedClass(repository, new ArrayList<>(RulesList.getChecks())); repository.done(); } @@ -42,28 +37,7 @@ public String repositoryKey() { } @Override - public List checkClasses() { - return Arrays.asList(CustomPythonVisitorCheck.class, CustomPythonSubscriptionCheck.class); - } - - String loadResource(String path) { - URL resource = getClass().getResource(path); - if (resource == null) { - throw new IllegalStateException("Resource not found: " + path); - } - return readResource(resource); - } - - static String readResource(URL resource) { - ByteArrayOutputStream result = new ByteArrayOutputStream(); - try (InputStream in = resource.openStream()) { - byte[] buffer = new byte[1024]; - for (int len = in.read(buffer); len != -1; len = in.read(buffer)) { - result.write(buffer, 0, len); - } - return new String(result.toByteArray(), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new IllegalStateException("Failed to read resource: " + resource, e); - } + public List> checkClasses() { + return new ArrayList<>(RulesList.getChecks()); } } diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/RulesList.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/RulesList.java new file mode 100644 index 0000000000..c00bc26d0f --- /dev/null +++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/RulesList.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012-2024 SonarSource SA - mailto:info AT sonarsource DOT com + * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license. + */ +package org.sonar.samples.python; + +import java.util.List; +import java.util.stream.Stream; +import org.sonar.plugins.python.api.PythonCheck; +import org.sonar.samples.python.checks.CustomPythonSubscriptionCheck; +import org.sonar.samples.python.checks.CustomPythonVisitorCheck; + +public final class RulesList { + + private RulesList() { + } + + public static List> getChecks() { + return Stream.concat( + getPythonChecks().stream(), + getPythonTestChecks().stream() + ).toList(); + } + + /** + * These rules are going to target MAIN code only + */ + public static List> getPythonChecks() { + return List.of( + CustomPythonSubscriptionCheck.class + ); + } + + /** + * These rules are going to target TEST code only + */ + public static List> getPythonTestChecks() { + return List.of( + CustomPythonVisitorCheck.class + ); + } +} diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheck.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheck.java index e5d19099f2..12cce48a29 100644 --- a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheck.java +++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheck.java @@ -4,17 +4,12 @@ */ package org.sonar.samples.python.checks; -import org.sonar.check.Priority; import org.sonar.check.Rule; import org.sonar.plugins.python.api.PythonSubscriptionCheck; import org.sonar.plugins.python.api.tree.ForStatement; import org.sonar.plugins.python.api.tree.Tree; -@Rule( - key = CustomPythonSubscriptionCheck.RULE_KEY, - priority = Priority.MINOR, - name = "Python subscription visitor check", - description = "desc") +@Rule(key = CustomPythonSubscriptionCheck.RULE_KEY) public class CustomPythonSubscriptionCheck extends PythonSubscriptionCheck { public static final String RULE_KEY = "subscription"; diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonVisitorCheck.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonVisitorCheck.java index cdfd46cd43..2590444699 100644 --- a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonVisitorCheck.java +++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonVisitorCheck.java @@ -4,16 +4,11 @@ */ package org.sonar.samples.python.checks; -import org.sonar.check.Priority; import org.sonar.check.Rule; import org.sonar.plugins.python.api.PythonVisitorCheck; import org.sonar.plugins.python.api.tree.FunctionDef; -@Rule( - key = CustomPythonVisitorCheck.RULE_KEY, - priority = Priority.MINOR, - name = "Python visitor check", - description = "desc") +@Rule(key = CustomPythonVisitorCheck.RULE_KEY) public class CustomPythonVisitorCheck extends PythonVisitorCheck { public static final String RULE_KEY = "visitor"; diff --git a/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/subscription.json b/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/subscription.json new file mode 100644 index 0000000000..f844dc182d --- /dev/null +++ b/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/subscription.json @@ -0,0 +1,13 @@ +{ + "title": "Python subscription visitor check", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "pitfall" + ], + "defaultSeverity": "Minor" +} \ No newline at end of file diff --git a/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/visitor.json b/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/visitor.json new file mode 100644 index 0000000000..280019f021 --- /dev/null +++ b/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/visitor.json @@ -0,0 +1,13 @@ +{ + "title": "Python visitor check", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "pitfall" + ], + "defaultSeverity": "Minor" +} \ No newline at end of file diff --git a/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRuleRepositoryTest.java b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRuleRepositoryTest.java index 6a54f0a738..fffc7adceb 100644 --- a/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRuleRepositoryTest.java +++ b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRuleRepositoryTest.java @@ -4,42 +4,28 @@ */ package org.sonar.samples.python; -import java.io.IOException; -import java.net.URL; import org.junit.Test; +import org.sonar.api.SonarEdition; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.internal.SonarRuntimeImpl; import org.sonar.api.server.rule.RulesDefinition; -import org.mockito.Mockito; +import org.sonar.api.utils.Version; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; public class CustomPythonRuleRepositoryTest { @Test public void test_rule_repository() { - CustomPythonRuleRepository customPythonRuleRepository = new CustomPythonRuleRepository(); + SonarRuntime sonarRuntime = SonarRuntimeImpl.forSonarQube(Version.create(9, 9), SonarQubeSide.SCANNER, SonarEdition.DEVELOPER); + CustomPythonRuleRepository customPythonRuleRepository = new CustomPythonRuleRepository(sonarRuntime); RulesDefinition.Context context = new RulesDefinition.Context(); customPythonRuleRepository.define(context); assertThat(customPythonRuleRepository.repositoryKey()).isEqualTo("python-custom-rules"); assertThat(context.repositories()).hasSize(1).extracting("key").containsExactly(customPythonRuleRepository.repositoryKey()); - assertThat(context.repositories().get(0).rules()).hasSize(2); + var rules = context.repositories().get(0).rules(); + assertThat(rules).hasSize(2); assertThat(customPythonRuleRepository.checkClasses()).hasSize(2); } - - @Test - public void test_unfound_resource(){ - assertThatThrownBy(() -> new CustomPythonRuleRepository().loadResource("/unknown")) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Resource not found: /unknown"); - } - - @Test - public void test_read_exception_resource() throws IOException { - URL urlMock = Mockito.mock(URL.class); - Mockito.when(urlMock.openStream()).thenThrow(IOException.class); - Mockito.when(urlMock.toString()).thenReturn("MyURL"); - assertThatThrownBy(() -> CustomPythonRuleRepository.readResource(urlMock)) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Failed to read resource: MyURL"); - } } diff --git a/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRulesPluginTest.java b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRulesPluginTest.java index 565b627791..68d2338099 100644 --- a/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRulesPluginTest.java +++ b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRulesPluginTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import org.sonar.api.Plugin; import org.sonar.api.SonarEdition; +import org.sonar.api.SonarProduct; import org.sonar.api.SonarQubeSide; import org.sonar.api.SonarRuntime; import org.sonar.api.internal.PluginContextImpl; @@ -18,9 +19,32 @@ public class CustomPythonRulesPluginTest { @Test public void test() { - SonarRuntime sonarRuntime = SonarRuntimeImpl.forSonarQube(Version.create(7, 9), SonarQubeSide.SCANNER, SonarEdition.DEVELOPER); + SonarRuntime sonarRuntime = SonarRuntimeImpl.forSonarQube(Version.create(9, 9), SonarQubeSide.SCANNER, SonarEdition.DEVELOPER); Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(sonarRuntime).build(); new CustomPythonRulesPlugin().define(context); assertThat(context.getExtensions()).hasSize(1); } + + public static class MockedSonarRuntime implements SonarRuntime { + + @Override + public Version getApiVersion() { + return Version.create(9, 9); + } + + @Override + public SonarProduct getProduct() { + return SonarProduct.SONARQUBE; + } + + @Override + public SonarQubeSide getSonarQubeSide() { + return SonarQubeSide.SCANNER; + } + + @Override + public SonarEdition getEdition() { + return SonarEdition.COMMUNITY; + } + } }