Show Navigation

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: 3.3.0

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.7 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

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 the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-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:

multiproject

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 or finally blocks.

  • Braces: How many times have you seen an if or else 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, an if 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:

/app/build.gradle
apply from: "${rootProject.projectDir}/gradle/codenarc.gradle"

We encapsulate all the CodeNarc configuration in gradle/codenarc.gradle:

/gradle/codenarc.gradle
apply plugin: 'codenarc' (1)

codenarc {
    toolVersion = '0.27.0' (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:

/config/codenarc/rules.groovy
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:

report1
  • 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 of UrlMappings 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:

/app/src/codenarc/rules.groovy
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:

/app/grails-app/controllers/demo/UrlMappings.groovy
@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:

report2

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:

/app/build.gradle
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 6.1.6.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.

/codenarc-rule/build.gradle
apply plugin: 'groovy'

repositories {
    jcenter()
}

dependencies {
    compile 'org.codenarc:CodeNarc:0.27.0' (1)
}
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:

/gradle/codenarc.gradle
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:

/codenarc-rule/src/main/resources/rulesets/grails-extra.xml
<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:

/codenarc-rule/src/main/resources/codenarc-messages.properties
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:

/codenarc-rule/src/main/groovy/org/codenarc/rule/grails/GrailsTransactionalRule.groovy
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:

/codenarc-rule/src/test/groovy/org/codenarc/rule/grails/GrailsTransactionalRuleTest.groovy
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.

/app/src/codenarc/rules.groovy
ruleset {
    description 'Grails-CodeNarc Project RuleSet'

    ...
    ruleset('rulesets/grails-extra.xml')
}

Now we can create a service and use Spring @Transactional on it:

/app/build.gradle
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:

report3

As we can see we have a new violation because the rule we created as been applied.

5 Do you need help with Grails?

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team