Skip to content

Commit

Permalink
SONARPY-2379 Update the custom rules in the sonar-python repository t…
Browse files Browse the repository at this point in the history
…o match LAYC format
  • Loading branch information
maksim-grebeniuk-sonarsource committed Nov 27, 2024
1 parent 42caced commit d0e3bcc
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 85 deletions.
28 changes: 23 additions & 5 deletions docs/python-custom-rule-examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
<description>Python custom rule examples for SonarQube</description>

<properties>
<sonar.python.version>3.15.0.9787</sonar.python.version>
<sonar.python.version>4.24.0.18631</sonar.python.version>
</properties>
<dependencies>
<dependency>
<groupId>org.sonarsource.sonarqube</groupId>
<groupId>org.sonarsource.api.plugin</groupId>
<artifactId>sonar-plugin-api</artifactId>
<version>7.9</version>
<version>10.12.0.2522</version>
<scope>provided</scope>
</dependency>
<dependency>
Expand All @@ -32,12 +32,28 @@
<version>${sonar.python.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.sonarsource.python</groupId>
<artifactId>python-checks-testkit</artifactId>
<version>${sonar.python.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.sonarsource.sonarqube</groupId>
<artifactId>sonar-plugin-api-impl</artifactId>
<version>10.7.0.96327</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.sonarsource.api.plugin</groupId>
<artifactId>sonar-plugin-api-test-fixtures</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand All @@ -59,6 +75,8 @@
<version>1.21.0.505</version>
<extensions>true</extensions>
<configuration>
<pluginKey>python-custom</pluginKey>
<pluginName>Python Custom Rules</pluginName>
<pluginClass>org.sonar.samples.python.CustomPythonRulesPlugin</pluginClass>
<requirePlugins>python:${sonar.python.version}</requirePlugins>
<sonarLintSupported>true</sonarLintSupported>
Expand All @@ -70,8 +88,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> 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();
}

Expand All @@ -42,28 +37,7 @@ public String repositoryKey() {
}

@Override
public List<Class> 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<Class<?>> checkClasses() {
return new ArrayList<>(RulesList.getChecks());
}
}
Original file line number Diff line number Diff line change
@@ -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<Class<? extends PythonCheck>> getChecks() {
return Stream.concat(
getPythonChecks().stream(),
getPythonTestChecks().stream()
).toList();
}

/**
* These rules are going to target MAIN code only
*/
public static List<Class<? extends PythonCheck>> getPythonChecks() {
return List.of(
CustomPythonSubscriptionCheck.class
);
}

/**
* These rules are going to target TEST code only
*/
public static List<Class<? extends PythonCheck>> getPythonTestChecks() {
return List.of(
CustomPythonVisitorCheck.class
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"title": "Python subscription visitor check",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"pitfall"
],
"defaultSeverity": "Minor"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"title": "Python visitor check",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"pitfall"
],
"defaultSeverity": "Minor"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
}

0 comments on commit d0e3bcc

Please sign in to comment.