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: