Skip to content

Commit

Permalink
green-code-initiative#92 refactor(rule/python): moves Python rules in…
Browse files Browse the repository at this point in the history
…to `ecocode-rules-specifications` module
  • Loading branch information
jycr committed Jul 3, 2023
1 parent ffdda7c commit 6f8f567
Show file tree
Hide file tree
Showing 20 changed files with 198 additions and 82 deletions.
19 changes: 19 additions & 0 deletions ecocode-rules-specifications/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@
</fileset>
<flattenmapper/>
</copy>
<copy todir="${project.build.outputDirectory}/io/ecocode/rules/python">
<fileset dir="${project.build.directory}/rules">
<include name="*/python/EC*.html"/>
<include name="*/EC*.json"/>
</fileset>
<flattenmapper/>
</copy>
</target>
</configuration>
</execution>
Expand Down Expand Up @@ -171,6 +178,18 @@
</descriptors>
</configuration>
</execution>
<execution>
<id>assembly-python</id>
<phase>prepare-package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>${project.basedir}/src/main/assembly/python.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
<configuration>
<appendAssemblyId>true</appendAssemblyId>
Expand Down
18 changes: 18 additions & 0 deletions ecocode-rules-specifications/src/main/assembly/python.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.1 https://maven.apache.org/xsd/assembly-2.1.1.xsd">
<id>python</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.build.outputDirectory}</directory>
<includes>
<include>io/ecocode/rules/python/*.*</include>
</includes>
<outputDirectory/>
</fileSet>
</fileSets>
</assembly>
14 changes: 14 additions & 0 deletions ecocode-rules-specifications/src/main/rules/EC10/EC10.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"title": "Avoid using unoptimized vector images",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "60min"
},
"tags": [
"eco-design",
"ecocode"
],
"defaultSeverity": "Minor"
}
16 changes: 16 additions & 0 deletions ecocode-rules-specifications/src/main/rules/EC203/EC203.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"title": "Detect unoptimized image format",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "60min"
},
"tags": [
"performance",
"user-experience",
"eco-design",
"ecocode"
],
"defaultSeverity": "Minor"
}
15 changes: 15 additions & 0 deletions ecocode-rules-specifications/src/main/rules/EC404/EC404.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"title": "Use generator comprehension instead of list comprehension in for loop declaration",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "15min"
},
"tags": [
"performance",
"eco-design",
"ecocode"
],
"defaultSeverity": "Minor"
}
15 changes: 15 additions & 0 deletions ecocode-rules-specifications/src/main/rules/EC7/EC7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"title": "Avoid creating getter and setter methods in classes",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"eco-design",
"performance",
"ecocode"
],
"defaultSeverity": "Minor"
}
13 changes: 13 additions & 0 deletions python-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,24 @@
<url>https://github.com/green-code-initiative/ecoCode/tree/main/python-plugin</url>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>ecocode-rules-specifications</artifactId>
<version>${project.version}</version>
<classifier>python</classifier>
</dependency>

<dependency>
<groupId>org.sonarsource.python</groupId>
<artifactId>sonar-python-plugin</artifactId>
<type>sonar-plugin</type>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.sonarsource.sonarqube</groupId>
<artifactId>sonar-plugin-api</artifactId>
<scope>provided</scope>
</dependency>

<!-- for security on regex patterns : https://github.com/google/re2j -->
Expand Down Expand Up @@ -63,6 +71,11 @@
<jreMinVersion>${java.version}</jreMinVersion>
</configuration>
</plugin>
<plugin>
<!-- shade plugin configuration is on parent pom.xml because of use for other modules -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,32 @@
package fr.greencodeinitiative.python;

import fr.greencodeinitiative.python.checks.*;
import org.sonar.api.rules.RuleType;
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.sonarsource.analyzer.commons.RuleMetadataLoader;

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.List;
import java.util.Map;

public class PythonRuleRepository implements RulesDefinition, PythonCustomRuleRepository {

public static final String LANGUAGE = "py";
public static final String NAME = "ecoCode";
public static final String RESOURCE_BASE_PATH = "/fr/greencodeinitiative/l10n/python/rules/python/";
public static final String RESOURCE_BASE_PATH = "io/ecocode/rules/python";
public static final String REPOSITORY_KEY = "ecocode-python";

@Override
public void define(Context context) {
NewRepository repository = context.createRepository(repositoryKey(), LANGUAGE).setName(NAME);

new RulesDefinitionAnnotationLoader().load(repository, checkClasses().toArray(new Class[] {}));

// technical debt
Map<String, String> remediationCosts = new HashMap<>();
remediationCosts.put(AvoidSQLRequestInLoop.RULE_KEY, "10min");
remediationCosts.put(AvoidFullSQLRequest.RULE_KEY, "20min");
repository.rules().forEach(rule -> {
rule.setType(RuleType.CODE_SMELL);
String debt = remediationCosts.get(rule.key());

// TODO DDC : create support to use org.apache.commons.lang.StringUtils
// if (StringUtils.isBlank(debt)) {
if (debt == null || debt.trim().equals("")) {
// default debt to 5min for issue correction
rule.setDebtRemediationFunction(
rule.debtRemediationFunctions().constantPerIssue("5min"));
} else {
rule.setDebtRemediationFunction(
rule.debtRemediationFunctions().constantPerIssue(debt));
}
});
private final SonarRuntime sonarRuntime;

// HTML description
repository.rules().forEach(rule ->
rule.setHtmlDescription(loadResource(RESOURCE_BASE_PATH + rule.key() + ".html")));
public PythonRuleRepository(SonarRuntime sonarRuntime) {
this.sonarRuntime = sonarRuntime;
}

@Override
public void define(Context context) {
NewRepository repository = context.createRepository(REPOSITORY_KEY, LANGUAGE).setName(NAME);
RuleMetadataLoader ruleMetadataLoader = new RuleMetadataLoader(RESOURCE_BASE_PATH, sonarRuntime);
ruleMetadataLoader.addRulesByAnnotatedClass(repository, (List) checkClasses());
repository.done();
}

Expand All @@ -92,21 +66,4 @@ public List<Class> checkClasses() {
DetectUnoptimizedImageFormat.class
);
}

private String loadResource(String path) {
URL resource = getClass().getResource(path);
if (resource == null) {
throw new IllegalStateException("Resource not found: " + path);
}
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: " + path, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,26 @@
*/
package fr.greencodeinitiative.python;

import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.sonar.api.Plugin;
import org.sonar.api.SonarRuntime;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

public class PythonPluginTest {
class PythonPluginTest {
private Plugin.Context context;

@Test
public void test() {
@BeforeEach
void init() {
SonarRuntime sonarRuntime = mock(SonarRuntime.class);
Plugin.Context context = new Plugin.Context(sonarRuntime);
context = new Plugin.Context(sonarRuntime);
new PythonPlugin().define(context);
}

@Test
void test() {
assertThat(context.getExtensions()).hasSize(1);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,88 @@
*/
package fr.greencodeinitiative.python;

import static org.assertj.core.api.Assertions.assertThat;

import org.assertj.core.api.SoftAssertions;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.sonar.api.SonarRuntime;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.api.utils.Version;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

public class PythonRuleRepositoryTest {
class PythonRuleRepositoryTest {

private PythonRuleRepository pythonRuleRepository;
private RulesDefinition.Context context;
private RulesDefinition.Repository repository;

@Before
public void init() {
pythonRuleRepository = new PythonRuleRepository();
context = new RulesDefinition.Context();
pythonRuleRepository.define(context);
repository = context.repository(PythonRuleRepository.REPOSITORY_KEY);
@BeforeEach
void init() {
// TODO: Remove this check after Git repo split
/*
On an IDE (like IntelliJ), if the developer runs the unit tests without building/generating the Maven goals on the
"ecocode-rules-specifications" module before, the unit tests will not see the generated HTML descriptions (from ASCIIDOC files).
The developer must therefore configure his IDE to build the `ecocode-rules-specifications` module before launching the Tests.
When the `python-plugin` submodule is in a specific Git repository, `ecocode-rules-specifications` will be fetched from a classic
external Maven dependency. There will therefore no longer be any need to perform this specific configuration.
*/
if (PythonRuleRepository.class.getResource("/io/ecocode/rules/python/EC4.json") == null) {
String message = "'ecocode-rules-specification' resources corrupted. Please check build of 'ecocode-rules-specification' module";
if (System.getProperties().keySet().stream().anyMatch(k -> k.toString().startsWith("idea."))) {
message += "\n\nOn 'IntelliJ IDEA':" +
"\n1. go to settings :" +
"\n > Build, Execution, Deployment > Build Tools > Maven > Runner" +
"\n2. check option:" +
"\n > Delegate IDE build/run actions to Maven" +
"\n3. Click on menu: " +
"\n > Build > Build Project"
;
}
fail(message);
}

final SonarRuntime sonarRuntime = mock(SonarRuntime.class);
doReturn(Version.create(0, 0)).when(sonarRuntime).getApiVersion();
PythonRuleRepository rulesDefinition = new PythonRuleRepository(sonarRuntime);
RulesDefinition.Context context = new RulesDefinition.Context();
rulesDefinition.define(context);
repository = context.repository(rulesDefinition.repositoryKey());
}

@Test
public void test() {
assertThat(pythonRuleRepository.repositoryKey()).isEqualTo(PythonRuleRepository.REPOSITORY_KEY);
assertThat(context.repositories()).hasSize(1).extracting("key").containsExactly(pythonRuleRepository.repositoryKey());
assertThat(context.repositories().get(0).rules()).hasSize(10);
assertThat(pythonRuleRepository.checkClasses()).hasSize(10);
@DisplayName("Test repository metadata")
void testMetadata() {
assertThat(repository.name()).isEqualTo("ecoCode");
assertThat(repository.language()).isEqualTo("py");
assertThat(repository.key()).isEqualTo("ecocode-python");
}

@Test
void testRegistredRules() {
assertThat(repository.rules()).hasSize(10);
}

/**
* Check all rule keys must be prefixed by 'EC'
*/
@Test
public void testRuleKeyPrefix() {
@DisplayName("All rule keys must be prefixed by 'EC'")
void testRuleKeyPrefix() {
SoftAssertions assertions = new SoftAssertions();
repository.rules().forEach(
rule -> assertions.assertThat(rule.key()).startsWith("EC")
);
assertions.assertAll();
}

@Test
void testAllRuleParametersHaveDescription() {
SoftAssertions assertions = new SoftAssertions();
repository.rules().stream()
.flatMap(rule -> rule.params().stream())
.forEach(param -> assertions.assertThat(param.description())
.as("description for " + param.key())
.isNotEmpty());
assertions.assertAll();
}
}

0 comments on commit 6f8f567

Please sign in to comment.