Skip to content

Commit deff704

Browse files
saberduckGabinL21
andauthored
SONARRUBY-82 Add ability to register rules in built-in profiles from other plugins (#63)
Co-authored-by: GabinL21 <67428953+GabinL21@users.noreply.github.com>
1 parent d156844 commit deff704

File tree

11 files changed

+371
-0
lines changed

11 files changed

+371
-0
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ sonar {
199199
property 'sonar.links.scm', "${sonarLinksScm}"
200200
property 'sonar.links.issue', 'https://jira.sonarsource.com/browse/SONARRUBY'
201201
property 'sonar.exclusions', '**/build/**/*'
202+
property 'sonar.coverage.exclusions', 'its/test-plugin/**/*'
202203
}
203204
}
204205

its/plugin/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ test {
1818
}
1919
systemProperty 'java.awt.headless', 'true'
2020
outputs.upToDateWhen { false }
21+
// Ensure test-plugin is built before running integration tests
22+
dependsOn ':its:test-plugin:build'
2123
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* SonarSource Ruby
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.slang;
18+
19+
import com.sonar.orchestrator.container.Edition;
20+
import com.sonar.orchestrator.junit4.OrchestratorRule;
21+
import com.sonar.orchestrator.junit4.OrchestratorRuleBuilder;
22+
import com.sonar.orchestrator.locator.FileLocation;
23+
import com.sonar.orchestrator.locator.Location;
24+
import com.sonar.orchestrator.locator.MavenLocation;
25+
import java.io.File;
26+
import java.util.List;
27+
import java.util.stream.Collectors;
28+
import org.apache.commons.lang.StringUtils;
29+
import org.junit.ClassRule;
30+
import org.junit.Test;
31+
import org.sonarqube.ws.client.HttpConnector;
32+
import org.sonarqube.ws.client.WsClient;
33+
import org.sonarqube.ws.client.WsClientFactories;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
37+
/**
38+
* Integration test that verifies RubyProfileRegistrar functionality by using a separate
39+
* Orchestrator instance with both the Ruby plugin and a test plugin that registers a custom rule.
40+
*/
41+
public class ProfileRegistrarTest {
42+
43+
@ClassRule
44+
public static final OrchestratorRule ORCHESTRATOR;
45+
46+
static {
47+
OrchestratorRuleBuilder orchestratorBuilder = OrchestratorRule.builderEnv();
48+
addRubyPlugin(orchestratorBuilder);
49+
addTestPlugin(orchestratorBuilder);
50+
ORCHESTRATOR = orchestratorBuilder
51+
.useDefaultAdminCredentialsForBuilds(true)
52+
.setEdition(Edition.ENTERPRISE_LW)
53+
.activateLicense()
54+
.setSonarVersion(System.getProperty("sonar.runtimeVersion", "LATEST_RELEASE"))
55+
.build();
56+
}
57+
58+
private static void addRubyPlugin(OrchestratorRuleBuilder builder) {
59+
String slangVersion = System.getProperty("slangVersion");
60+
61+
Location pluginLocation;
62+
String plugin = "sonar-ruby-plugin";
63+
if (StringUtils.isEmpty(slangVersion)) {
64+
// use the plugin that was built on local machine
65+
pluginLocation = FileLocation.byWildcardMavenFilename(
66+
new File("../../" + plugin + "/build/libs"),
67+
plugin + "-*-all.jar");
68+
} else {
69+
// QA environment downloads the plugin built by the CI job
70+
pluginLocation = MavenLocation.of("org.sonarsource.slang", plugin, slangVersion);
71+
}
72+
73+
builder.addPlugin(pluginLocation);
74+
}
75+
76+
private static void addTestPlugin(OrchestratorRuleBuilder builder) {
77+
// Always use the test plugin that was built on local machine
78+
String testPlugin = "test-plugin";
79+
Location testPluginLocation = FileLocation.byWildcardMavenFilename(
80+
new File("../test-plugin/build/libs"),
81+
testPlugin + "-*.jar");
82+
builder.addPlugin(testPluginLocation);
83+
}
84+
85+
private static WsClient newWsClient() {
86+
return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
87+
.url(ORCHESTRATOR.getServer().getUrl())
88+
.build());
89+
}
90+
91+
@Test
92+
public void testPluginRegistersRuleInDefaultRubyProfile() {
93+
var wsClient = newWsClient();
94+
95+
// First, query the quality profiles to find the profile key for Ruby's "Sonar way" profile
96+
var profileSearchRequest = new org.sonarqube.ws.client.qualityprofiles.SearchRequest()
97+
.setLanguage("ruby");
98+
99+
var profileResponse = wsClient.qualityprofiles().search(profileSearchRequest);
100+
101+
var rubyProfile = profileResponse.getProfilesList().stream()
102+
.filter(profile -> "Sonar way".equals(profile.getName()) && profile.getIsBuiltIn())
103+
.findFirst()
104+
.orElseThrow(() -> new AssertionError("Built-in 'Sonar way' profile not found for Ruby language"));
105+
106+
var profileKey = rubyProfile.getKey();
107+
108+
// Query the active rules in the built-in "Sonar way" profile for Ruby language using the profile key
109+
var rulesSearchRequest = new org.sonarqube.ws.client.rules.SearchRequest()
110+
.setLanguages(List.of("ruby"))
111+
.setQprofile(profileKey)
112+
.setActivation("true");
113+
114+
var rulesResponse = wsClient.rules().search(rulesSearchRequest);
115+
116+
// Verify that the profile contains the TEST001 rule from ruby-test repository
117+
// This rule is registered by the TestProfileRegistrar in the test-plugin
118+
var testRules = rulesResponse.getRulesList().stream()
119+
.filter(rule -> "ruby-test:TEST001".equals(rule.getKey()))
120+
.collect(Collectors.toList());
121+
122+
assertThat(testRules)
123+
.as("Rule ruby-test:TEST001 should be registered in the default Ruby profile by TestProfileRegistrar")
124+
.hasSize(1);
125+
126+
var testRule = testRules.get(0);
127+
assertThat(testRule.getKey()).isEqualTo("ruby-test:TEST001");
128+
assertThat(testRule.getRepo()).isEqualTo("ruby-test");
129+
}
130+
}
131+

its/test-plugin/build.gradle

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
plugins {
2+
id 'java'
3+
}
4+
5+
dependencies {
6+
compileOnly libs.sonar.plugin.api
7+
compileOnly project(':sonar-ruby-plugin')
8+
}
9+
10+
jar {
11+
manifest {
12+
def displayVersion = (project.buildNumber == null ? project.version : project.version.substring(0, project.version.lastIndexOf('.')) + " (build ${project.buildNumber})")
13+
def buildDate = new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
14+
attributes(
15+
'Build-Time': buildDate,
16+
'Plugin-Class': 'org.sonarsource.ruby.testplugin.TestPlugin',
17+
'Plugin-Description': 'Minimalistic Test Plugin',
18+
'Plugin-Developers': 'SonarSource Team',
19+
'Plugin-Display-Version': displayVersion,
20+
'Plugin-Key': 'testplugin',
21+
'Plugin-License': 'LGPL v3',
22+
'Plugin-Name': 'Test Plugin',
23+
'Plugin-Organization': 'SonarSource',
24+
'Plugin-Version': project.version,
25+
'Sonar-Version': '6.7',
26+
'Version': "${project.version}",
27+
)
28+
}
29+
}
30+
31+
// Skip this submodule from SonarQube analysis
32+
sonarqube.skipProject = true
33+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* SonarSource Ruby
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.ruby.testplugin;
18+
19+
import org.sonar.api.Plugin;
20+
21+
/**
22+
* Minimalistic SonarQube plugin for testing purposes.
23+
*/
24+
public class TestPlugin implements Plugin {
25+
26+
@Override
27+
public void define(Context context) {
28+
// Register the rules definition that defines the TEST001 rule
29+
context.addExtension(TestRulesDefinition.class);
30+
// Register the profile registrar that adds the rule to the default Ruby quality profile
31+
context.addExtension(TestProfileRegistrar.class);
32+
}
33+
}
34+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* SonarSource Ruby
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.ruby.testplugin;
18+
19+
import java.util.List;
20+
import org.sonar.api.rule.RuleKey;
21+
import org.sonar.plugins.ruby.api.RubyProfileRegistrar;
22+
23+
/**
24+
* Test implementation of RubyProfileRegistrar that adds a single rule to the default quality profile.
25+
*/
26+
public class TestProfileRegistrar implements RubyProfileRegistrar {
27+
28+
@Override
29+
public void register(RegistrarContext registrarContext) {
30+
RuleKey ruleKey = RuleKey.of(TestRulesDefinition.REPOSITORY_KEY, TestRulesDefinition.RULE_KEY);
31+
registrarContext.registerDefaultQualityProfileRules(List.of(ruleKey));
32+
}
33+
}
34+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* SonarSource Ruby
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.ruby.testplugin;
18+
19+
import org.sonar.api.server.rule.RulesDefinition;
20+
21+
/**
22+
* Defines a test rule repository with a single TEST001 rule.
23+
*/
24+
public class TestRulesDefinition implements RulesDefinition {
25+
26+
static final String REPOSITORY_KEY = "ruby-test";
27+
static final String LANGUAGE_KEY = "ruby";
28+
private static final String REPOSITORY_NAME = "Test Ruby Rules";
29+
static final String RULE_KEY = "TEST001";
30+
31+
@Override
32+
public void define(Context context) {
33+
NewRepository repository = context.createRepository(REPOSITORY_KEY, LANGUAGE_KEY)
34+
.setName(REPOSITORY_NAME);
35+
36+
// Define the TEST001 rule
37+
repository.createRule(RULE_KEY)
38+
.setName("Test Rule 001")
39+
.setHtmlDescription("This is a test rule for integration testing purposes.");
40+
41+
repository.done();
42+
}
43+
}
44+

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,5 @@ rootProject.name = 'sonar-ruby'
6767
include 'sonar-ruby-plugin'
6868
include 'its:plugin'
6969
include 'its:ruling'
70+
include 'its:test-plugin'
7071
include 'jruby-repackaged'
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* SonarSource Ruby
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.ruby.api;
18+
19+
import java.util.Collection;
20+
import org.sonar.api.Beta;
21+
import org.sonar.api.rule.RuleKey;
22+
import org.sonar.api.server.ServerSide;
23+
24+
/**
25+
* This class can be extended to provide additional rule keys in the builtin default quality profile.
26+
*
27+
* <pre>
28+
* {@code
29+
* public void register(RegistrarContext registrarContext) {
30+
* registrarContext.registerDefaultQualityProfileRules(ruleKeys);
31+
* }
32+
* }
33+
* </pre>
34+
*
35+
*/
36+
@Beta
37+
@ServerSide
38+
public interface RubyProfileRegistrar {
39+
/**
40+
* This method is called on server side and during an analysis to modify the builtin default quality profile for Ruby.
41+
*/
42+
void register(RegistrarContext registrarContext);
43+
44+
interface RegistrarContext {
45+
/**
46+
* Registers additional rules into the "Sonar Way" default quality profile for Ruby.
47+
*
48+
* @param ruleKeys additional rule keys
49+
*/
50+
void registerDefaultQualityProfileRules(Collection<RuleKey> ruleKeys);
51+
}
52+
}

sonar-ruby-plugin/src/main/java/org/sonarsource/ruby/plugin/RubyProfileDefinition.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,33 @@
1616
*/
1717
package org.sonarsource.ruby.plugin;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import org.sonar.api.rule.RuleKey;
1922
import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
23+
import org.sonar.plugins.ruby.api.RubyProfileRegistrar;
2024
import org.sonarsource.analyzer.commons.BuiltInQualityProfileJsonLoader;
2125

2226
public class RubyProfileDefinition implements BuiltInQualityProfilesDefinition {
2327

2428
static final String PATH_TO_JSON = "org/sonar/l10n/ruby/rules/ruby/Sonar_way_profile.json";
29+
private final List<RuleKey> additionalRules = new ArrayList<>();
30+
31+
public RubyProfileDefinition() {
32+
this(new RubyProfileRegistrar[]{});
33+
}
34+
35+
public RubyProfileDefinition(RubyProfileRegistrar[] registrars) {
36+
for (var registrar : registrars) {
37+
registrar.register(additionalRules::addAll);
38+
}
39+
}
2540

2641
@Override
2742
public void define(Context context) {
2843
NewBuiltInQualityProfile profile = context.createBuiltInQualityProfile(RubyPlugin.PROFILE_NAME, RubyPlugin.RUBY_LANGUAGE_KEY);
2944
BuiltInQualityProfileJsonLoader.load(profile, RubyPlugin.RUBY_REPOSITORY_KEY, PATH_TO_JSON);
45+
additionalRules.forEach(ruleKey -> profile.activateRule(ruleKey.repository(), ruleKey.rule()));
3046
profile.done();
3147
}
3248

0 commit comments

Comments
 (0)