第一步:创建插件应用
mvn archetype:generate
-DgroupId=com.yy
-DartifactId=sonar-yy-plugin
-Dpackage=com.yy
-Dversion=1.0.0-SNAPSHOT
-DarchetypeGroupId=org.apache.maven.archetypes
-DarchetypeArtifactId=maven-archetype-quickstart
-DarchetypeVersion=1.4
-DinteractiveMode=false
第二步:配置pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yy</groupId>
<artifactId>sonar-yy-plugin</artifactId>
<!-- 注意这里的packaging-->
<packaging>sonar-plugin</packaging>
<version>1.0</version>
<name>sonar-yy-plugin</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<sonar.version>9.4.0.54424</sonar.version>
<sonarqube.version>9.9.0.229</sonarqube.version>
<sonarjava.version>7.16.0.30901</sonarjava.version>
<analyzer.commons.version>2.5.0.1358</analyzer.commons.version>
</properties>
<dependencies>
<!-- groupId has changed to 'org.sonarsource.api.plugin' starting on version 9.5 -->
<dependency>
<groupId>org.sonarsource.sonarqube</groupId>
<artifactId>sonar-plugin-api</artifactId>
<version>${sonar.version}</version>
<scope>provided</scope>
</dependency>
<!--用于基于java的自定义规则-->
<dependency>
<groupId>org.sonarsource.java</groupId>
<artifactId>sonar-java-plugin</artifactId>
<type>sonar-plugin</type>
<version>4.7.1.9272</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.sonarsource.analyzer-commons</groupId>
<artifactId>sonar-analyzer-commons</artifactId>
<version>${analyzer.commons.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
<artifactId>sonar-packaging-maven-plugin</artifactId>
<version>1.17</version>
<extensions>true</extensions>
<configuration>
<pluginDescription>test</pluginDescription>
<pluginKey>java-custom</pluginKey>
<pluginName>Java Custom Rules</pluginName>
<!-- 重要:这个类就是下文定义的插件类 -->
<pluginClass>com.yy.YYPlugin</pluginClass>
<pluginDescription>how to write sonar plugin</pluginDescription>
<sonarLintSupported>true</sonarLintSupported>
<sonarQubeMinVersion>${sonarqube.version}</sonarQubeMinVersion>
<requirePlugins>java:${sonarjava.version}</requirePlugins>
</configuration>
</plugin>
</plugins>
</build>
</project>
第三步:开发规则
package com.yy.checks;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
import java.util.Collections;
import java.util.List;
/**
* 规则:方法名称起始字母小写.
* 这里的key会起到关键字作用,后面要创建的文件名与其一致。
*/
@Rule(key = "MethodNameStartLowerCase")
public class MethodNameStartLowerCaseRule extends IssuableSubscriptionVisitor {
@Override
public List<Tree.Kind> nodesToVisit() {
return Collections.singletonList(Tree.Kind.METHOD);
}
@Override
public void visitNode(Tree tree) {
MethodTree method = (MethodTree) tree;
//获取方法名
String methodName = method.simpleName().name() ;
if(!Character.isLowerCase(methodName.charAt(0))){
//规则未通过,生成Issue
reportIssue(method.simpleName(), "方法首字母必须小写");
}
super.visitNode(tree);
}
}
package com.yy.checks;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.*;
import java.util.List;
/**
* 规则:避免注解
*/
@Rule(key = "AvoidAnnotation")
public class AvoidAnnotationRule extends BaseTreeVisitor implements JavaFileScanner {
private static final String DEFAULT_VALUE = "Inject";
private JavaFileScannerContext context;
/**
* Name of the annotation to avoid. Value can be set by users in Quality profiles.
* The key
*/
@RuleProperty(
defaultValue = DEFAULT_VALUE,
description = "Name of the annotation to avoid, without the prefix @, for instance 'Override'")
protected String name;
@Override
public void scanFile(JavaFileScannerContext context) {
this.context = context;
scan(context.getTree());
}
@Override
public void visitMethod(MethodTree tree) {
List<AnnotationTree> annotations = tree.modifiers().annotations();
for (AnnotationTree annotationTree : annotations) {
TypeTree annotationType = annotationTree.annotationType();
if (annotationType.is(Tree.Kind.IDENTIFIER)) {
IdentifierTree identifier = (IdentifierTree) annotationType;
if (identifier.name().equals(name)) {
context.reportIssue(this, identifier, String.format("Avoid using annotation @%s", name));
}
}
}
// The call to the super implementation allows to continue the visit of the AST.
// Be careful to always call this method to visit every node of the tree.
super.visitMethod(tree);
}
}
第四步:创建规则元数据
在resources目录下创建“rules/java”目录,并在里面创建MethodNameStartLowerCase.html、MethodNameStartLowerCase.json、AvoidAnnotation.html和AvoidAnnotation.json文件。
1、MethodNameStartLowerCase.html
等同帮助文档,以帮助开发人员解决问题。
<p>该规则检测方法的名称是否已小写字母开头</p>
<h2>不符合规范示例代码</h2>
<pre>
class MyClass {
int DoSomething(int a) { // 方法名称首字母为大写
return 42;
}
}
</pre>
<h2>符合规范示例代码</h2>
<pre>
class MyClass {
int doSomething(int a) { // 方法名称首字母为小写
return 42;
}
}
</pre>
2、MethodNameStartLowerCase.json
规则的配置信息
{
"title": "Method names start with lowercase letters",
"type": "CODE_SMELL",
"status": "ready",
"tags": [
"method",
"bugs"
],
"defaultSeverity": "Critical"
}
3、AvoidAnnotation.html
<p>This rule detects usage of configured annotation</p>
<h2>Noncompliant Code Example</h2>
<pre>
TO DO
</pre>
<h2>Compliant Solution</h2>
<pre>
TO DO
</pre>
4、AvoidAnnotation.json
{
"title": "Title of AvoidAnnotation",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"pitfall"
],
"defaultSeverity": "Minor"
}
第五步:激活规则
这里的激活指的是将哪些规则收集应用
package com.yy;
import com.yy.checks.AvoidAnnotationRule;
import com.yy.checks.MethodNameStartLowerCaseRule;
import com.yy.checks.NoIfStatementInTestsRule;
import org.sonar.plugins.java.api.JavaCheck;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 规则集。 统一在这里地方将规则收集,是因为后面有两个地方会用到这些规则
*/
public final class RulesList {
private RulesList() {}
public static List<Class<? extends JavaCheck>> getChecks() {
List<Class<? extends JavaCheck>> checks = new ArrayList<>();
checks.addAll(getJavaChecks());
checks.addAll(getJavaTestChecks());
return Collections.unmodifiableList(checks);
}
/**
* These rules are going to target MAIN code only
*/
public static List<Class<? extends JavaCheck>> getJavaChecks() {
return Collections.unmodifiableList(Arrays.asList(
MethodNameStartLowerCaseRule.class,
AvoidAnnotationRule.class));
}
/**
* These rules are going to target TEST code only
*/
public static List<Class<? extends JavaCheck>> getJavaTestChecks() {
return Collections.unmodifiableList(Arrays.asList(
NoIfStatementInTestsRule.class));
}
}
第六步:定义编码规则
到此为止,上面的过程创建了规则运行代码、创建了规则的元数据、将规则进行了分类存储,那么接下来便要将这些规则集成到sonar中去进行应用。
package com.yy;
import org.sonar.api.SonarRuntime;
import org.sonar.api.config.Configuration;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonarsource.analyzer.commons.RuleMetadataLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
/**
* 定义相同存储库的一些编码规则。例如,Java Findbugs插件提供了这个扩展点的实现,以便定义它所支持的规则。
* 在服务器规则存储库中声明规则元数据。这允许在“规则”页面中列出规则。
* 说明:RulesDefinition接口取代了已弃用的类org.sonar.api.rules.RuleRepository
*/
public class MyRulesDefinition implements RulesDefinition {
/**
* 核心组件org.sonar.api.config.Configuration提供对配置的访问。
* 它处理默认值和值的解密。它可用于所有栈(扫描器,web服务器,计算引擎)
*/
private final Configuration config;
private final SonarRuntime runtime;
// Add the rule keys of the rules which need to be considered as template-rules
private static final Set<String> RULE_TEMPLATES_KEY = Collections.emptySet();
//规则的元数据和json文件所在路径
private static final String RESOURCE_BASE_PATH = "rules/java";
//规则所属的存储库关键字
public static final String REPOSITORY_KEY = "custom-repo";
//规则所属的存储库名称
public static final String REPOSITORY_NAME = "My Custom Repository";
public MyRulesDefinition(Configuration config,SonarRuntime runtime){
this.config = config ;
this.runtime = runtime ;
}
/**
* 此方法在服务器启动时执行
*/
@Override
public void define(Context context) {
//实例化一个新仓库,指定这个规则适应的编程语言
final NewRepository repository = context.createRepository(REPOSITORY_KEY, "java")
.setName(REPOSITORY_NAME);
//通过规则元数据进行加载的Loader
RuleMetadataLoader ruleMetadataLoader = new RuleMetadataLoader(RESOURCE_BASE_PATH, runtime);
//添加规则,同时会检查是否有添加了@Rule注解,以及根据key解析对应的html和json文件
ruleMetadataLoader.addRulesByAnnotatedClass(repository, new ArrayList<>(RulesList.getChecks()));
setTemplates(repository);
repository.done();
}
private static void setTemplates(NewRepository repository) {
RULE_TEMPLATES_KEY.stream()
.map(repository::rule)
.filter(Objects::nonNull)
.forEach(rule -> rule.setTemplate(true));
}
}
第七步:注册规则
因为定义的规则依赖于Java API的SonarSource Analyzer,所以还需要告诉父插件必须检索一些新规则。
package com.yy;
import org.sonar.plugins.java.api.CheckRegistrar;
import org.sonarsource.api.sonarlint.SonarLintSide;
/**
* 因为我们的规则依赖于Java API的SonarSource Analyzer,
* 所以还需要告诉父Java插件必须检索一些新规则。
*/
@SonarLintSide
public class MyCheckRegistrar implements CheckRegistrar {
/**
* 在分析期间调用此方法以获取用于实例化检查的类。
* @param registrarContext java插件将使用这个上下文来检索要检查的类
*/
@Override
public void register(RegistrarContext registrarContext) {
registrarContext
.registerClassesForRepository(MyRulesDefinition.REPOSITORY_KEY
,RulesList.getJavaChecks()
,RulesList.getJavaTestChecks());
}
}
第八步:定义插件
插件将扩展注入SonarQube的入口点
package com.yy;
import com.yy.sensors.CustomSensor;
import com.yy.sensors.Foo;
import org.sonar.api.Plugin;
import org.sonar.api.config.PropertyDefinition;
/**
* SonarQube为它的三个技术栈提供了扩展点:
* - Scanner : 运行源代码分析
* - Compute Engine:它整合扫描器的输出,例如
* 1)计算二级指标,如评级
* 2)聚合度量(例如项目的代码行数=所有文件的代码行数之和)
* 3)将新问题分配给开发人员
* 4)将所有内容持久化到数据存储中
* - Web application
* 扩展点不是用来添加新特性的,而是用来完善现有特性的。
* 从技术上讲,它们是由Java接口或带有@ExtensionPoint注释的抽象类定义的契约。
* 插件提供的扩展点(命名的扩展)的实现必须在其入口点类中声明,
* 该入口点类实现org.sonar.api.Plugin接口并在pom.xml中引用
*
* 由以下三个注解分别标注应该被执行的环境(栈)
* 1) @ScannerSide:扫描仪运行时
* 2) @ServerSide:web服务器
* 3) @ComputeEngineSide:计算引擎
*
* 例如,一个scanner sensor只能在scanner runtime被实例化和执行,
* 而不在web服务器(web server)和计算引擎中(Compute Engine)
*/
public class YYPlugin implements Plugin {
@Override
public void define(Context context) {
//插件可以定义自己的属性,以便可以从web管理控制台配置它们。
//必须使用扩展点org.sonar.api.config.PropertyDefinition
//也可以在扩展上使用注释org.sonar.api.config.PropertyDefinition来声明属性。
context.addExtension(
PropertyDefinition.builder("sonar.my.property")
.name("My Property")
.description("This is the description displayed in web admin console")
.defaultValue("42")
.build()
);
//添加扩展
context.addExtensions(MyRulesDefinition.class
,MyCheckRegistrar.class) ;
}
}
第九步:注册插件
将应用进行打包,得到插件的jar包,sonar-yy-plugin-1.0.jar
拷贝该jar包到Sonar Qube安装路径下的“extensions\plugins”中,然后重启Sonar即可
第十步:查看规则
打开sonar页面,切换到“代码规则”菜单,输入规则的关键字进行搜索,如下所示
点击右侧的规则进入
点击激活规则
更多自定义规则说明请移步:https://github.com/SonarSource/sonar-java/blob/master/docs/CUSTOM_RULES_101.md
补充说明
上面在定义规则的时候使用了html和json文件来描述插件的信息,除此以外,我们还可以直接在代码中进行描述,即“以编程的方式定义规则”,例如:
package com.yy;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.rule.RuleParamType;
import org.sonar.api.server.rule.RulesDefinition;
/**
* 以编程方式定义规则
*/
public class MyCodeRulesDefinition implements RulesDefinition {
@Override
public void define(Context context) {
// 规则库
NewRepository repository = context.createRepository("my_js", "js")
.setName("My Javascript Analyzer");
// 以编程方式定义规则. 注意,规则可以从文件中加载(JSON, XML,…)
NewRule x1Rule = repository.createRule("x1")
.setName("No empty line")
.setHtmlDescription("Generate an issue on empty lines")
// optional tags
.setTags("style", "stupid")
// optional status. Default value is READY.
.setStatus(RuleStatus.BETA)
// default severity when the rule is activated on a Quality profile. Default value is MAJOR.
.setSeverity(Severity.MINOR)
// optional type for SonarQube Quality Model. Default is RuleType.CODE_SMELL.
.setType(RuleType.BUG) ;
//用于计算问题修复成本
x1Rule.setDebtRemediationFunction(
x1Rule.debtRemediationFunctions()
.linearWithOffset("1h", "30min"));
//创建参数
x1Rule.createParam("acceptWhitespace")
.setDefaultValue("false")
.setType(RuleParamType.BOOLEAN)
.setDescription("Accept whitespaces on the line");
// don't forget to call done() to finalize the definition
repository.done();
}
}