Build your custom rules

Introduction

The ObjectScript analyzer parses the source code, creates an Abstract Syntax Tree (AST) and then walks through the entire tree. A coding rule can subscribe to be notified every time a node of a certain type is visited.

As soon as the coding rule is notified, it can navigate the tree around the node and raise issues if necessary.

Here you will learn to create your own rule for ObjectScript code.

Tools

In this tutorial we use Java JDK 11 and Maven. If you intend to deploy your plugin in SonarQube 7.7 or earlier, you should use JDK 8 instead.

Go to GitHub and clone the following project to your bucket:

https://github.com/litesolutions/objectscript-custom-rules

Writing a Plugin

Writing new ObjectScript coding rules is a six-step process:

  • Create a standard SonarQube plugin.
  • Attach this plugin to the SonarQube ObjectScript plugin (see the pom.xml file of the provided sample plugin project).
  • Create as many custom ObjectScript coding rules as required by extending org.sonar.plugins.objectscript.api.check.ObjectScript*Check and add them to the previous repository.
  • Generate the SonarQube plugin (jar file).
  • Place this jar file in the $SONARQUBE_HOME/extensions/plugins directory.
  • Restart the SonarQube server.

Plugin Project Sample

To get started, clone the sample plugin project and follow the steps below:

  • Install Maven
  • Build the plugin by running mvn package from the project directory. This will generate a SonarQube plugin jar file in the target directory.
  • Add your newly created jar into the $SONARQUBE_HOME/extensions/plugins directory
  • Restart the SonarQube server

If you now look at the objectscript quality profiles, you will find the new coding rule ("Detect whether a _local_ variable name has a length >= 20 characters"). Don’t forget to activate it and run an analysis of a objectscript project.

Main structure

Then main structure is as follows:

The ObjectscriptCustomRulesPlugin.java file is the main application point, and is the one that will declare the extension entry. In our example it is MyObjecscriptRules.java.

The MyObjecscriptRules.java file is where you declare all the rules you have developed, which must be declared in the checkClasses method:

public ImmutableList<Class> checkClasses() {
    return ImmutableList.of(ExampleCheck.class, VariableNameLengthCheck.class);
}

Additionally, you need to set the remediation cost for each rule in the define method:

public void define(Context context) {
    NewRepository repository = context.createRepository(repositoryKey(), "objectscript").setName("MyCompany Custom Repository");

    // Load rule meta data from annotations
    RulesDefinitionAnnotationLoader annotationLoader = new RulesDefinitionAnnotationLoader();
    checkClasses().forEach(ruleClass -> annotationLoader.load(repository, ruleClass));

    // Optionally override html description from annotation with content from html files
    repository.rules().forEach(rule -> rule.setHtmlDescription(loadResource("/org/sonar/l10n/objectscript/rules/" + rule.key() + ".html")));

    // Optionally define remediation costs
    Map<String, String> remediationCosts = new HashMap<>();
    remediationCosts.put(ExampleCheck.KEY, "5min");
    remediationCosts.put(VariableNameLengthCheck.KEY, "5min");
    repository.rules().forEach(rule -> rule.setDebtRemediationFunction(
      rule.debtRemediationFunctions().constantPerIssue(remediationCosts.get(rule.key()))));

    repository.done();
}

As you can see in previous function there is expected to find a html file for each rule at src/main/resources/org/sonar/l10n/objectscript/rules/ path. The file name must be the rule key.

Finally, in checks folder is where you will create your custom rules.

Understand how rules works

Your rules must be created in checks folders and you can create 3 different type of checks:


Prent class
 
Type
org.sonar.plugins.objectscript.api.check.ObjectScriptCheck General check
org.sonar.plugins.objectscript.api.check.ObjectScriptClassCheck Class check
org.sonar.plugins.objectscript.api.check.ObjectScriptMethodCheck Method check

Overridable methods

A coding rule can optionally override four methods inherited from any of previous classes.

init

This method is called only once and should be used to subscribe to one or more NodeType(s).

Very often when writing a coding rule, you will want to subscribe to a NodeType. A NodeType can be either a rule of the grammar or a keyword of the language. As an example, here is the code of the implementation of the “Package %ZEN is deprecated” coding rule:

@Override
public void init()
{
    subscribeTo(References.CLASS);
}

In that case, will filter nodes that are references to a class.

For ObjectScriptClassCheck and ObjectScriptMethodCheck classes this method is already defined, so you don't need to redefine this method is you  are inheriting from any of thoses classes.

visitNode

This method is called when an AstNode matches a subscribed NodeType and before analyzing its content.

Usage example:

@Override
public void visitNode(final AstNode astNode)
{
    if(astNode.getTokenValue().startsWith("%ZEN")){
        getContext().createLineViolation(this, MESSAGE, astNode);
    }
}

leaveNode

This method is called when an AstNode matches a desired NodeType and after analyzing its content.

Usage example:

@Override
public void leaveNode(final AstNode astNode)
{
    final int complexity = getMethod().getInt(SqMetricDefs.COMPLEXITY);

    if (complexity <= complexityThreshold)
        return;

    final String msg = String.format(MESSAGE, complexity,
        complexityThreshold);
    getContext().createLineViolation(this, msg, astNode);
}

destroy

This method is called before shutting down the coding rule. 

Usage example

@Override
public void destroy() {
    for (final MethodInfo deadMethod : nameMethods) {
        generateViolation(deadMethod.className, deadMethod.sourceCode, deadMethod.tokenLine);
    }
}

Create your first rule

Le'ts take the VariableNameLengthCheck as an example for your first rule.

In VariableNameLengthCheck we inherit from ObjectScriptMethodCheck as we want to get access to the local variables defined in methods.

The check is done in visitNode method, wich is mandatory. In this method we must define the conditions that must be accomplished by the source code to analyze. In case some condition is not achived, a new code violation will be reported.

@Override
public void visitNode(final AstNode astNode)
{
    AstNode nodes = getMethod().getBody();
    for (final AstNode node: nodes.getDescendants(Variables.LOCAL)) {
     if(node.getTokenValue().length()> 20)
        getContext().createLineViolation(this, String.format(MESSAGE,
                node.getTokenValue()), node);
    }
}

Let's analyze the method step by step.

AstNode nodes = getMethod().getBody();

Obtain the current method content as an AST.

for (final AstNode node: nodes.getDescendants(Variables.LOCAL)) {

Loop on each local variable child. The getDescendants method accepts to use filters to return only desired elements. If you want to use multiple filters, add a parameter for each node type you want to get.

if(node.getTokenValue().length()> 20)

Check if the name has more than 20 characters.

getContext().createLineViolation(this, String.format(MESSAGE, node.getTokenValue()), node);

Create a line violation for the current node.

Create rule help

The rule help file must be created in src/main/resources/org/sonar/l10n/objectscript/rules/.

The rule help file name must be the key declared in the rule file. For example, in VariableNameLengthCheck is:

public static final String KEY = "CRQ0002";

In that case, the help file must be CRQ0002.html.

The rule help file must contain HTML formatted text, without the <html>, <head> and <body> tags.

Unit test

We strongly recommend to create Unit Test for your rules. The test structure is as follows:

In resources folder there must be a folder for each rule key to test.

Your rule test must inherit from OsqBaseTestCheck. Then you must override method checkData to include the validations to check.

If we take a look to checkData in VariableNameLengthCheckTest class, it refers to the resource C1.cls, which is expected to be found on src/test/resources/CRQ002, where CRQ002 is the key of the VariableNameLengthCheck:

protected Iterator<Object[]> checkData()
{
    final List<Object[]> list = new ArrayList<>();

    String resourceName;
    ViolationList violationList;

    resourceName = "C1.cls";
    violationList = ViolationList.create()
        .add(5, String.format(VariableNameLengthCheck.MESSAGE,    "statusverylongexample"))
        .add(7, String.format(VariableNameLengthCheck.MESSAGE,    "statusverylongexamplemore"))
        .build();
    list.add(new Object[] { resourceName, violationList });

  
    return list.iterator();
}

In our test we expect to get a violation in line 5 and another one in line 7 of C1.cls file.

Available methods

Class ObjectScriptMethodCheck

Method Visibility Return type Description
getEnclosingClass() protected ObjectScriptClass Gets the ObjectScriptClass of the ObjectScriptMethod object associated to the method.
getMethod() protected ObjectScriptMethod Gets the ObjectScriptMethod object associated to the method.

Class ObjectScriptClassCheck

Method Visibility Return type Description
getCosClass() protected ObjectScriptClass Gets the ObjectScriptClass of the class.

Class ObjectScriptMethod

Method Visibility Return type Description
getMethodName() public String Returns the name of the method, or class method.
getArguments() public List<MethodArgument> Returns the list of arguments for this method.
getBody() public AstNode Get the body of the method.
getModifiers() public List<AstNode> Returns the list of modifiers for this method, if any. If none, the result will be an empty list.
getModifierValue(MethodModifier) public String Get a method modifier value as a string, if any.
getNode() public AstNode Returns the raw node for this method. This node will include all of the method declaration and body.
getReturnType() public AstNode Returns the declared return type of this method, or class method, if any. If no return type is declared, this method returns {@code null}.
is(MethodModifier) public boolean Tells whether a given method has a boolean modifier set to true.
@throws IllegalArgumentException this modifier is not a boolean modifier.
isClassMethod() public boolean Returns true if this source code instance is a class method, not a method.
isMethod() public boolean Returns true if this source code instance is a method, not a class method.
isNot(MethodModifier) public boolean Tells whether a given method has a boolean modifier set to false.
@throws IllegalArgumentException this modifier is not a boolean modifier.

Class ObjectScriptClass

Method Visibility Return type Description
getBody() public AstNode Returns the class's body.
getClassName() public String Returns the name of the class.
getElements(AstNodeType ...) public List<AstNode> Returns the elements of that class having a certain type, if any. If none, the result will be an empty list.
getExtended() public Set<String> Returns the list of classes (as string values) which this class extends.
getMethods() public Collection<ObjectScriptMethod> Returns the list of {@code Method} of {@code ClassMethod} in that class
getModifiers() public List<AstNode> Returns the list of modifiers for this method, if any. If none, the result will be an empty list.
getNode() public AstNode Returns the raw node of this class.

Additional information

You will also be familiar with some SonarQube classes. Next we refer the most importants: