Static code analysis in a Grails app with CodeNarc
In this guide, you'll learn how to improve your code with static analysis using CodeNarc.
Authors: Iván López
Grails Version: 4.0.1
1 Grails Training
Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.
2 Getting Started
When we write code, it’s important to follow some rules, right practices, style rules, etc. However, sometimes it is not that simple. It is even more important when we work in a team, in which every member has his preferences. One way of improving this is adding a static analysis tool for the code.
In this guide, you are going to install and configure Codenarc to help you improve the quality of your Grails code, and you are also going to learn how to create a custom CodeNarc rule. CodeNarc analyzes the Groovy code and reports potential bugs and code problems.
2.1 What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
2.2 How to complete the guide
To get started do the following:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/grails-codenarc.git
The Grails guides repositories contain two folders:
-
initial
Initial project. Often a simple Grails app with some additional code to give you a head-start. -
complete
A completed example. It is the result of working through the steps presented by the guide and applying those changes to theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-guides/grails-codenarc/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/grails-codenarc/complete
|
3 Writing the Application
3.1 Multi-Project Build
We are going to setup a multi-project build with a Grails application and a custom CodeNarc Rule as shown in the next image:
Check our Grails Multi-Project Build Guide, to learn more.
3.2 Rule Types
Current CodeNarc version (0.27.0) includes 348 rules divided in 22 categories:
-
Basic: For example to check that there are no empty
else
orfinally
blocks. -
Braces: How many times have you seen an
if
orelse
with only one statement without the curly-braces? I personally don’t like code without curly-braces because it’s a source of bugs in the future. We can add the rules in this category to perform these checks. -
Convention: There are rules to check for some conventions: when we write an "inverted"
if
, anif
that can be converted to an elvis operator,… -
Exceptions: Rules that will fail if, for example, we throw a
NullPointerException
.
And there are many more categories to check for duplicated imports, unused variables, unnecessary ifs,… And of course there’s a specific category for Grails rules.
3.3 Adding CodeNarc to our project
Adding CodeNarc to our project is a simple task because there’s a Gradle plugin to do it.
Let’s modify build.gradle
and add the following:
apply from: "${rootProject.projectDir}/gradle/codenarc.gradle"
We encapsulate all the CodeNarc configuration in gradle/codenarc.gradle
:
apply plugin: 'codenarc' (1)
codenarc {
toolVersion = '1.4' (2)
configFile = file("${rootProject.projectDir}/config/codenarc/rules.groovy") (3)
reportFormat = 'html' (4)
ignoreFailures = true (5)
}
1 | Apply the codenarc plugin. |
2 | Set the CodeNarc version we want to use. |
3 | Define the main file with the rules. By default CodeNarc uses config/codenarc/codenarc.xml but we don’t want to
write XML files, don’t we? |
4 | The report format we want. For a human-readable format we use html . If we want to integrate CodeNarc with, for
example, Jenkins we need to change it to xml . |
5 | We don’t want that the build fails when there’s only one violation. |
Then we need to create the rules file:
ruleset {
description 'Grails-CodeNarc Project RuleSet'
ruleset('rulesets/basic.xml')
ruleset('rulesets/braces.xml')
ruleset('rulesets/convention.xml')
ruleset('rulesets/design.xml')
ruleset('rulesets/dry.xml')
ruleset('rulesets/exceptions.xml')
ruleset('rulesets/formatting.xml')
ruleset('rulesets/generic.xml')
ruleset('rulesets/imports.xml')
ruleset('rulesets/naming.xml')
ruleset('rulesets/unnecessary.xml')
ruleset('rulesets/unused.xml')
ruleset('rulesets/grails.xml')
}
With this configuration we can just run the check
task.
$ ./gradlew app:check
:check UP-TO-DATE
:complete:codenarcMain
CodeNarc rule violations were found. See the report at: file:///home/ivan/workspaces/oci/guides/grails-codenarc/complete/app/build/reports/codenarc/main.html
:complete:codenarcTest NO-SOURCE
:complete:compileJava NO-SOURCE
:complete:compileGroovy UP-TO-DATE
:complete:buildProperties UP-TO-DATE
:complete:processResources UP-TO-DATE
:complete:classes UP-TO-DATE
:complete:compileTestJava NO-SOURCE
:complete:compileTestGroovy NO-SOURCE
:complete:processTestResources NO-SOURCE
:complete:testClasses UP-TO-DATE
:complete:test NO-SOURCE
:complete:check
Total time: 1.953 secs
And we can open the test report to check the violations:
-
First, we have a section with the execution date and the CodeNarc version used.
-
Then there’s another section with a Summary with the total of files with violations and also the number of violations with priority 1, 2 and 3.
-
After that, there’s a section for each file in which we can see all the violations in the file with the line of code and a small fragment of it. The rule name is a link to a more detailed explanation.
Let’s fix the violations
There are different ways of fixing a CodeNarc violation:
-
Fixing the problem: As the name implies, just fix the violation.
-
Disabling the rule: Sometimes we don’t agree with that specific CodeNarc violation. We can disable it.
-
Ignoring the rule for that specific class or method: This case is when we don’t want to disable the rule but want to skip it only in a particular class or method.
Fix the problem
-
To fix the violation
FileEndsWithoutNewline
add a new line at the end ofUrlMappings
file. -
To fix the violation
SpaceBeforeOpeningBrace
add a space before the braces.
Disable the rule
For the case of ClassJavadoc
and NoDef
we can just disable the rules modifying the rules.groovy
file:
ruleset {
description 'Grails-CodeNarc Project RuleSet'
...
ruleset('rulesets/convention.xml') {
'NoDef' {
enabled = false
}
}
...
ruleset('rulesets/formatting.xml') {
'ClassJavadoc' {
enabled = false
}
}
...
}
Ignoring a rule
Finally, we can ignore a rule for a particular class. In this case, we’re going to ignore the UnnecessaryGString
rule
for UrlMappings.groovy
using @SuppressWarnings
:
@SuppressWarnings(['UnnecessaryGString'])
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?" {
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
}
}
Checking the result
Just execute again the check
task and open the report to see that all the violations are gone:
3.4 Final configuration
If you install CodeNarc in a medium-big size project, which has not use any static analysis tool before, you may have hundreds or event thousands of violations. It is possible that your team has not been careful enough while writing code. Moreover, it is likely that CodeNarc default configuration contains some rules you don’t agree with. Rules which might not be a good fit for the team.
My advice is to review all the violations, read their documentation and then decide if you want to disable them, configure with another option and finally fix and respect them.
Once the team decides the rules and the configuration the next step is to choose the thresholds to make the build fail. These thresholds allow having successful builds with some violations but, once we reach these levels, the build will fail.
To do that, just edit the build.gradle
file:
codenarc {
...
// We need to remove the following line
//ignoreFailures = true
maxPriority1Violations = 0
maxPriority2Violations = 5
maxPriority3Violations = 9
}
With this configuration the build will fail once we reach these thresholds.
4 Writing a custom Rule
Although CodeNarc provides some rules specific for Grails, some times we may want to create our own rules.
In this example we’re going to create a rule to check that we use
grails.gorm.transactions.Transactional
instead of
@org.springframework.transaction.annotation.Transactional
.
As you probably know the Grails @Transactional
annotation is better because it doesn’t create a runtime proxy. It’s an AST Transformation that it’s applied during compilation time, so there’s no runtime overhead. There’s also other features that the Grails annotation provides and
the Spring one don’t.
grails.gorm.transactions.Transactional is only available for the latest versions of GORM (this guide uses GORM 7.0.2.RELEASE), for previous versions you should use @grails.transaction.Transactional .
|
The rule checks our code. If we use the Spring annotation, it adds a new violation to the report.
4.1 Creating the Rule
Create the project
We need to create a new Groovy project because we want to package the rule in its own jar file using Gradle.
plugins {
id 'groovy'
}
repositories {
jcenter()
}
dependencies {
implementation 'org.codenarc:CodeNarc:1.4' (1)
testCompile 'junit:junit:4.12'
}
1 | Only need CodeNarc dependency |
In order to be able to user this new CodeNarc rule in our Grails application we need to make sure that the jar file
is available in the classpath during the codenarcMain
taks:
codenarcMain.dependsOn ':codenarc-rule:jar' (1)
tasks.withType(CodeNarc) { (2)
codenarcClasspath += files("${rootProject.projectDir}/codenarc-rule/build/libs/codenarc-rule.jar")
}
1 | CodeNarc tasks depend on the jar being generated |
2 | Add the jar to the codenarcClasspath |
Define the rule
The first step is create a meta-information file with the information about the rule we want to create:
<ruleset xmlns="http://codenarc.org/ruleset/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://codenarc.org/ruleset/1.0 http://codenarc.org/ruleset-schema.xsd"
xsi:noNamespaceSchemaLocation="http://codenarc.org/ruleset-schema.xsd">
<description>Extra Grails rules</description> (1)
<rule class='org.codenarc.rule.grails.GrailsTransactionalRule'/> (2)
</ruleset>
1 | The description of the group of rules. |
2 | One rule element per rule we create. |
Second we create a properties file with the description of the rule in both plain text and html. The messages will be used in the html report:
GrailsTransactional.description=Check that @org.springframework.transaction.annotation.Transactional is used instead of org.springframework.transaction.annotation.Transactional.
GrailsTransactional.description.html=Check that <em>@grails.gorm.transactions.Transactional</em> is used instead of <em>org.springframework.transaction.annotation.Transactional</em>.
Implement the rule
Finally, we implement the rule:
package org.codenarc.rule.grails
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.AnnotatedNode
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ImportNode
import org.codehaus.groovy.ast.ModuleNode
import org.codenarc.rule.AbstractAstVisitor
import org.codenarc.rule.AbstractAstVisitorRule
@CompileStatic
class GrailsTransactionalRule extends AbstractAstVisitorRule { (1)
int priority = 2 (2)
String name = 'GrailsTransactional' (3)
Class astVisitorClass = GrailsTransactionalVisitor (4)
}
@CompileStatic
class GrailsTransactionalVisitor extends AbstractAstVisitor { (5)
private static final String SPRING_TRANSACTIONAL = 'org.springframework.transaction.annotation.Transactional'
private static final String ERROR_MSG = 'Do not use Spring @Transactional, use @grails.gorm.transactions.Transactional instead'
@Override
void visitAnnotations(AnnotatedNode node) { (6)
node.annotations.each { AnnotationNode annotationNode ->
String annotation = annotationNode.classNode.text
if (annotation == SPRING_TRANSACTIONAL) {
addViolation(node, ERROR_MSG)
}
}
super.visitAnnotations(node)
}
@Override
void visitImports(ModuleNode node) { (7)
node.imports.each { ImportNode importNode ->
String importClass = importNode.className
if (importClass == SPRING_TRANSACTIONAL) {
node.lineNumber = importNode.lineNumber (8)
addViolation(node, ERROR_MSG)
}
}
super.visitImports(node)
}
}
1 | The rule needs to extend from AbstractAstVisitorRule |
2 | Define the violation priority |
3 | The name of the rule. It needs to be the same as defined previously in the meta-information file |
4 | The class that implements the rule |
5 | The visitor class needs to extend from AbstractAstVisitor |
6 | Check the annotations of the class and methods and in case that Spring @Transactional is used, add a new violation |
7 | Check the imports of the class and is case of Spring @Transactional is imported, add a new violation |
8 | It’s important to set the lineNumber of the node because if not, the violation won’t be added |
Testing
And of course we need to write tests to make sure that everything is working as expected:
package org.codenarc.rule.grails
import org.codenarc.rule.AbstractRuleTestCase
import org.codenarc.rule.Rule
import org.codenarc.rule.Violation
import org.junit.Test
class GrailsTransactionalRuleTest extends AbstractRuleTestCase { (1)
@Override
protected Rule createRule() { (2)
return new GrailsTransactionalRule()
}
@Test
void testGrailsTransactionalIsAllowedOnClassWithImport() {
final SOURCE = ''' (3)
import grails.gorm.transactions.Transactional
@Transactional
class TestService {
}
'''
assertNoViolations(SOURCE) (4)
}
@Test
void testGrailsTransactionalIsAllowedOnClassWithFullpackageAnnotation() {
final SOURCE = '''
@grails.gorm.transactions.Transactional
class TestService {
}
'''
assertNoViolations(SOURCE)
}
@Test
void testGrailsTransactionalIsAllowedOnMethodWithImport() {
final SOURCE = '''
import grails.gorm.transactions.Transactional
class TestService {
@Transactional
void foo() {
}
}
'''
assertNoViolations(SOURCE)
}
@Test
void testGrailsTransactionalIsAllowedOnMethodWithFullpackageAnnotation() {
final SOURCE = '''
class TestService {
@grails.gorm.transactions.Transactional
void foo() {
}
}
'''
assertNoViolations(SOURCE)
}
@Test
void testSpringTransactionalIsNotAllowedOnClassWithImport() {
final SOURCE = '''
import org.springframework.transaction.annotation.Transactional
@Transactional
class TestService {
}
'''
(5)
assertSingleViolation(SOURCE) { Violation violation ->
violation.rule.priority == 2 &&
violation.rule.name == 'GrailsTransactional'
}
}
@Test
void testSpringTransactionalIsNotAllowedOnClassWithFullpackageAnnotation() {
final SOURCE = '''
@org.springframework.transaction.annotation.Transactional
class TestService {
}
'''
assertSingleViolation(SOURCE) { Violation violation ->
violation.rule.priority == 2 &&
violation.rule.name == 'GrailsTransactional'
}
}
@Test
void testSpringTransactionalIsNotAllowedOnMethodWithImport() {
final SOURCE = '''
import org.springframework.transaction.annotation.Transactional
class TestService {
@Transactional
void foo() {
}
}
'''
assertSingleViolation(SOURCE) { Violation violation ->
violation.rule.priority == 2 &&
violation.rule.name == 'GrailsTransactional'
}
}
@Test
void testSpringTransactionalIsNotAllowedOnMethodWithFullpackageAnnotation() {
final SOURCE = '''
class TestService {
@org.springframework.transaction.annotation.Transactional
void foo() {
}
}
'''
assertSingleViolation(SOURCE) { Violation violation ->
violation.rule.priority == 2 &&
violation.rule.name == 'GrailsTransactional'
}
}
}
1 | The test class needs to extend from AbstractRuleTestCase |
2 | Instantiate the rule we want to test |
3 | Write the source code we want to test as a String |
4 | In this case, as we use Grails @Transactional we expect no violations |
5 | As we use Spring @Transactional we expect to have one violation |
4.2 Checking the Rule in the Grails application
Once we have created the rule and it’s available on the classpath, it’s time to add it to the Grails application.
ruleset {
description 'Grails-CodeNarc Project RuleSet'
...
ruleset('rulesets/grails-extra.xml')
}
Now we can create a service and use Spring @Transactional
on it:
package demo
import org.springframework.transaction.annotation.Transactional
class DemoService {
@Transactional
void myMethod() {
println 'Some business logic'
}
}
And the final step is generating the CodeNarc report:
As we can see we have a new violation because the rule we created as been applied.