I am an enthusiast about clean code and architecture. Together with testing and best practices I believe they build good and robust software. I really like to use static analysis tools to enforce code guidelines.
I recently started working on a Java project where we decided to introduce a static analysis tool to enforce different rules. Additionally (and most importantly), we wanted to enforce some specific architectural rules. We specifically wanted to forbid in a package to import anything outside of it within the same project codebase. I was the one in carrying out the implementation, so I thought:
Easy! I will just write a Lint rule!
As it happens sometimes, it was not as easy as I thought.
In this post I will show how I have setup CheckStyle in a Java project, the problems I had with missing documentation and what worked for me.
What is Static Code Analysis?
Before jumping into the how, let us talk about Static Code Analysis. What is it?
Static Code Analysis is a method to analyze source code without executing it. It is done by analyzing the code against a set of predefined rules. Nowadays, IDEs even have some of these tools integrated. By introducing static analysis tools, they provide immediate feedback during the development phase. The earlier we find issues in the lifecycle of development the better as it reduces the cost and effort needed to apply the necessary changes to comply with the rules.
Apart from the benefits in code quality, another big benefit of using static anaylsis tools is that they reduce the noise in code reviews. It makes code reviews less tedious. There are fewer issues or irrelevant things to be mentioned.
Such tools can also be used to enforce styling guidelines. Styling changes are a tremendous source of noise and getting rid of them has a big impact when reviewing someone else’s changes. Also, manually checking that agreed code guidelines are completely fulfilled is almost impossible to guarantee.
I particularly appreciate having these kind of rules when I do code reviews 😁.
Static analysis tools
Lint is a static code analysis tool used to find errors and bugs among other things. Since the aforementioned project is built using Maven, I started doing some Googling. I am used to using the Lint library provided by Google for Android, so I thought there may be something similar for Java projects.
After some research, I came to the conclusion that CheckStyle was a widely used tool and that it is very similar to the Lint tool I am familiar with. So let’s go with it! 🚀
Configuring the dependencies
In order to use CheckStyle in our project, we need to include it as an external dependency. If you wonder about its license, CheckStyle has a free software license: GNU Lesser General Public License (LGPL)
First of all, we need to define the dependencies in our pom.xml
:
<project>
<properties>
<checkstyle-maven-plugin.version>3.1.2</checkstyle-maven-plugin.version>
<checkstyle.version>8.41</checkstyle.version>
<checkstyle.config.location>config/checkstyle/config.xml</checkstyle.config.location>
<checkstyle.suppressions.location>config/checkstyle/suppressions.xml</checkstyle.suppressions.location>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle-maven-plugin.version}</version>
<configuration>
<consoleOutput>true</consoleOutput>
</configuration>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.9.1</version>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle-maven-plugin.version}</version>
<reportSets>
<reportSet>
<reports>
<report>checkstyle</report>
</reports>
<configuration>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
</configuration>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
</project>
Here there are a few things happening:
- We define the plugins
maven-checkstyle-plugin
andmaven-site-plugin
.- Inside of the plugin
maven-checkstyle-plugin
we also definecheckstyle
as dependency. - We run CheckStyle in the
validate
phase.
- Inside of the plugin
- We declare the plugin
maven-checkstyle-plugin
in thereporting
section.
Properties
Because we have the maven-checkstyle-plugin
declared in two separated places, we define a property checkstyle-maven-plugin.version
that holds the version number and we use it below when declaring the plugin:
<properties>
<checkstyle-maven-plugin.version>3.1.2</checkstyle-maven-plugin.version>
</properties>
Since we declared a property for the maven-checkstyle-plugin
version, why not doing the same for its checkstyle
dependency?
So we also declare a checkstyle.version
property with the version of the checkstyle
dependency:
<properties>
<checkstyle.version>8.41</checkstyle.version>
</properties>
As shown in the pom.xml
snippet, we can reference the properties like this: ${checkstyle-maven-plugin.version}
.
Setup location of configuration files
Configuration file
Interestingly, the documentation of CheckStyle’s plugin says that we can configure the location of the configuration file in a configuration
section in the plugin declaration, but that did not work for me for some reason.
What worked was to use the project’s parameter checkstyle.config.location
.
So I declared a parameter with that name and I put there the path to the configuration file, relative to the project root’s directory:
<properties>
<checkstyle.config.location>config/checkstyle/config.xml</checkstyle.config.location>
</properties>
Note: Usually, there are two configurations (Google and Sun) embedded already within the plugin and the sun_checks.xml
configuration will be used by default.
My approach does not use any embedded configuration and instead defines just a few rules.
If we want to use one of the default configurations and we also want to add some custom rules, we may have to copy the default configuration into our own file and use that as a single source of truth for configuring CheckStyle.
Suppressions file
The suppresions file is where we declare some exceptions where we do not want CheckStyle to report any violations. My suppressions file is currently empty, but I have configured it so I have it in place in case I need it. Let us be pragmatic! It would be annoying to set it up only when we need it since we would then have to dive into the documentation again. So I would rather just set it up now and have it empty.
The suppressions file is supposed to be declared similarly as the configuration file but that also did not work, so I declared it the same way using the property checkstyle.suppressions.location
:
<properties>
<checkstyle.suppressions.location>config/checkstyle/suppressions.xml</checkstyle.suppressions.location>
</properties>
Import control file
The import-control
file is where we define our import rules.
Its location is defined within the configuration file.
Below we will see it more in detail.
Configure CheckStyle
Configuration file
Below we can see an example of how I have set up my CheckStyle configuration.
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="error"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/config_filefilters.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->
<module name="SuppressionFilter">
<property name="file" value="config/checkstyle/suppressions.xml"/>
<property name="optional" value="false"/>
</module>
<!-- Java Files -->
<module name="TreeWalker">
<!-- Imports -->
<module name="ImportControl">
<property name="severity" value="error"/>
<property name="file" value="config/checkstyle/import-control.xml"/>
</module>
<module name="AvoidStarImport"/>
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<!-- Modifiers -->
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<!-- Long constants are defined with an uppercase ell. That is 'L' and not 'l'. -->
<module name="UpperEll"/>
</module>
</module>
Let us go step by step explaining everything that we are doing here:
- First of all we declare the
Checker
module for checking our files. - We consider all our files to be
UTF-8
encoded. - We declare the severity of all the findings as error.
- We want to analyze all
java
,properties
andxml
files. - We exclude all
module-info.java
files. - We setup the suppressions file as mandatory.
- The TreeWalker analyzes the
java
files:- We define our
import-control
set of rules and set their severity toerror
. - We add rules for avoiding star imports, redundant imports and unused imports.
- We add rules for modifier’s order and redundant modifiers.
- We add a rule for long constants so they are defined with an uppercase ell. That is
L
and notl
.
- We define our
I took most of this configuration from here.
Import-control file
Below we can see an example of how I have set up my import control configuration.
<?xml version="1.0"?>
<!DOCTYPE import-control PUBLIC
"-//Puppy Crawl//DTD Import Control 1.4//EN"
"http://checkstyle.sourceforge.net/dtds/import_control_1_4.dtd">
<import-control pkg="org.example" strategyOnMismatch="allowed">
<subpackage name="base">
<allow pkg="org\.example\.base\.[^.]+" regex="true"/>
<disallow pkg="org\.example\.[^.]+" regex="true"/>
<subpackage name="data">
<allow pkg="org\.example\.base\.(data|domain).*" regex="true"/>
<disallow pkg="org\.example\.base\.presentation.*" regex="true"/>
</subpackage>
<subpackage name="presentation">
<allow pkg="org\.example\.base\.(domain|presentation).*" regex="true"/>
<disallow pkg="org\.example\.base\.data.*" regex="true"/>
</subpackage>
<subpackage name="domain">
<allow pkg="org\.example\.base\.domain.*" regex="true"/>
<disallow pkg="org\.example\.base\.(data|presentation).*" regex="true"/>
</subpackage>
</subpackage>
</import-control>
In the above code we have a simple setup for Clean Architecture as an example.
Such configuration aims to enforce Clean Architecture’s separation of layers in the base
package.
Usually such separation occurs at root level, but for the purpose of this example I decided to nest it inside the base
package to display how the configuration looks like for nested subpackages.
In this example there are three layers: domain
, data
and presentation
.
Something worth noting is that CheckStyle will start searching for matching rules from the deepest package level and from top to bottom of the file. This means that once a rule is met in a subpackage, it will skip the rest of the rules from parent packages. Similarly, following the order within a subpackage, once a rule is met, it will skip the rest of the rules below.
For meeting Clean Architecture’s rules, we defined the following rules:
- In
base
package:- Allow all imports from
org.example.base
package. - Disallow all imports from
org.example.*
package. It looks contradictory with the previous rule, but we should remember that this rule is omitted once the previous is met.
- Allow all imports from
- In
base.data
package:- Allow all imports from
org.example.base.data
andorg.example.base.domain
packages. - Disallow all imports from
org.example.base.presentation
.
- Allow all imports from
- In
base.presentation
package:- Allow all imports from
org.example.base.presentation
andorg.example.base.domain
packages. - Disallow all imports from
org.example.base.data
.
- Allow all imports from
- In
base.domain
package:- Allow all imports from
org.example.base.domain
. - Disallow all imports from
org.example.base.data
andorg.example.base.presentation
packages.
- Allow all imports from
Suppressions file
We can find a sample of the suppressions file along with more information in the documentation.
IntelliJ plugin
Having CheckStyle run automatically when we build our project is very handy, it will warn us early if we violate any rules. However, it needs a manual task to run. This is where the CheckStyle-IDEA plugin for IntelliJ comes in handy. The plugin will display warnings in the IDE for rule violations without the need to run the analysis manually.
We can configure it to use our CheckStyle configuration file to enforce our rules.
For that we need to go in IntelliJ as of version 2021.1 to: Settings > Tools > CheckStyle
.
There we can configure the plugin to use the same CheckStyle version and the path of our configuration file.
This configuration will be stored in .idea/checkstyle-idea.xml
and we can add it to our GIT changeset.
Additionally, I would recommend to ignore the folder .idea/checkstyleidea-libs
in the .gitignore
file as this is where some libraries are copied by the plugin when performing some analysis.
Although I have focused on the IntelliJ plugin, there are similar plugins for other IDEs like VS Code too.
Summary
In this post we learned how to setup CheckStyle in our Java project, we have setup a basic configuration and we have configured the CheckStyle-IDEA plugin for IntelliJ.
Thanks for reading!
References
- CheckStyle official site
- CheckStyle IntelliJ IDE plugin
- What is Static Analysis?
- Wikipedia: Static Program Analysis
If you found this article interesting, share it!