Custom Gradle Plugin for Unified Static Code Analysis
Tags: tutorial, java, codequality, gradle
Published: 2023-02-04 16:08:57

[Static code analysis](https://dev.to/kirekov/why-you-need-static-code-analysis-28po) is an incredible technique that makes your code base easier to maintain. But if you have multiple services in different repositories (possibly developed by separate teams), how can you make everybody follow the stated code style? A good approach is encapsulating all rules within a single plugin that'll perform the required validations on each project build automatically.

![Meme cover](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/idf3kudgzi2dkd1zekof.jpg)

So, in this article I'm showing you:

1. How you can create the Gradle plugin with custom PMD and Checkstyle rules.
1. How to publish it to [plugins.gradle.org](https://plugins.gradle.org/).
1. How to automate the releasing process with GitHub Actions.

You can check out the code examples in [this repository](https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example).

## PMD, Checkstyle, and difficulties with polyrepos

[PMD](https://pmd.github.io/) and [Checkstyle](https://checkstyle.sourceforge.io/) are static analysis tools that check your code on each project build. Gradle allows to apply them easily.

```groovy
plugins {
    id 'java'
    id 'pmd'
    id 'checkstyle'
}
```

And now you can tune each plugin the way you want to.

```groovy
checkstyle {
    toolVersion = '10.5.0'
    ignoreFailures = false
    maxWarnings = 0
    configFile = file(pathToCheckstyleConfig)
}

pmd {
    consoleOutput = true
    toolVersion = '6.52.0'
    ignoreFailures = false
    ruleSetFiles = file(pathToPmdConfig)
}
```

If your entire project (or even company) is the [monorepository](https://monorepo.tools/), then this setup is absolutely fine. You just need to put these configurations in the root `build.gradle` file to apply those plugins for every existing module. But what if your choice is [polyrepository](https://earthly.dev/blog/monorepo-vs-polyrepo/)? What if you want to share the same code style within all the projects in the company that developers are working on (and all the ones the programmers will create in the future)? Well, you can tell them to simply [copy and paste](https://en.wikipedia.org/wiki/Copy-and-paste_programming) the plugins’ configuration. Anyway, that’s an error-prone approach. There is always a probability that somebody does misconfiguration.

As a matter of fact, we need to reuse somehow the defined code style configuration in every viable project. The answer is simple. We need a custom Gradle plugin to encapsulate PMD and Checkstyle rules.

## Custom Gradle plugin

### Build configuration

Look at the `build.gradle` declaration below. It's the basic setup of the Gradle plugin project.

```groovy
plugins {
    id 'java-gradle-plugin'
    id 'com.gradle.plugin-publish' version '1.1.0'
}

group = 'io.github.simonharmonicminor.code.style'
sourceCompatibility = '8'

repositories {
    mavenCentral()
}

ext {
    set('lombokVersion', '1.18.24')
}

dependencies {
    compileOnly "org.projectlombok:lombok:${lombokVersion}"
    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
}

gradlePlugin {
    website = 'https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example'
    vcsUrl = 'https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example'
    plugins {
        gradleCodeStylePluginExample {
            id = 'io.github.simonharmonicminor.code.style'
            displayName = 'Gradle Plugin Code Style Example'
            description = 'Predefined Checkstyle and PMD rules'
            implementationClass = 'io.github.simonharmonicminor.code.style.CodingRulesGradlePluginPlugin'
            tags.set(['codestyle', 'checkstyle', 'pmd'])
        }
    }
}

tasks.named('test') {
    useJUnitPlatform()
}
```

Now let’s deconstruct the configuration step by step, starting with `plugins` block. Look at the code snippet below.

```groovy
plugins {
    id 'java-gradle-plugin'
    id 'com.gradle.plugin-publish' version '1.1.0'
}
```

The `java-gradle-plugin` command enables the tasks for the regular Gradle plugin project. Whilst the `com.gradle.plugin-publish` one allows to pack and publish the plugin to https://plugins.gradle.org/.

> I'm showing you the whole publishing process lately.

Then comes the basic project configuration.

```groovy
group = 'io.github.simonharmonicminor.code.style'
sourceCompatibility = '8'

repositories {
    mavenCentral()
}
```

The `group` defines `groupId` in favor of [Apache Maven naming conventions](https://maven.apache.org/guides/mini/guide-naming-conventions.html). The `sourceCompatibility` is the version of the target Java binaries. Though Java 8 is outdated now, I recommend you to build your Gradle plugins with the earliest JDK version that developers use in your company. Otherwise, you’ll block them the way to follow your code style guidelines.

Then comes the `dependencies` scope.

```groovy
ext {
    set('lombokVersion', '1.18.24')
}

dependencies {
    compileOnly "org.projectlombok:lombok:${lombokVersion}"
    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
}
```

Nothing special here. So, let's move on to the publishing configuration.

```groovy
gradlePlugin {
    website = 'https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example'
    vcsUrl = 'https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example'
    plugins {
        gradleCodeStylePluginExample {
            id = 'io.github.simonharmonicminor.code.style'
            displayName = 'Gradle Plugin Code Style Example'
            description = 'Predefined Checkstyle and PMD rules'
            implementationClass = 'io.github.simonharmonicminor.code.style.CodingRulesGradlePluginPlugin'
            tags.set(['codestyle', 'checkstyle', 'pmd'])
        }
    }
}
```

The `website` and `vcsUrl` should point out to the public Git repository with the source code of the plugin. And the `plugins` block defines each implementation of `Plugin` interface in your project. Finally, the `tags` are just hashtags to search your plugin in the registry.

> When you publish your Gradle plugin to https://plugins.gradle.org/, the package name is crucial. The code of your plugin should be available on GitHub. If it’s not open source, you may have issues with publishing it. Then you can declare your package name as `io.github.your_github_login.any.package.you.like`. But if you wish to use some other name like `com.mycompany.my.plugin`, then make sure you owe the domain `mycompany.com`. Otherwise, the Gradle engineers may reject the publishing.

> Note that Gradle prohibits `plugin` and `gradle` as tags values. Such builds fail during `gradle publishPlugins` task execution.

And finally comes the JUnit 5 configuration.

```groovy
tasks.named('test') {
    useJUnitPlatform()
}
```

### The plugin code

I want to show you the whole plugin code. And when I'm going to explain each individual detail to you. Look at the code snippet below.

```java
public class CodingRulesGradlePluginPlugin implements Plugin<Project> {

  @Override
  public void apply(Project project) {
    project.getPluginManager().apply("checkstyle");
    project.getExtensions().configure(CheckstyleExtension.class, checkstyleExtension -> {
      checkstyleExtension.setToolVersion("10.5.0");
      checkstyleExtension.setIgnoreFailures(false);
      checkstyleExtension.setMaxWarnings(0);
      checkstyleExtension.setConfigFile(
          FileUtil.copyContentToTempFile("style/checkstyle.xml", ".checkstyle.xml")
      );
    });

    project.getPluginManager().apply("pmd");
    project.getExtensions().configure(PmdExtension.class, pmdExtension -> {
      pmdExtension.setConsoleOutput(true);
      pmdExtension.setToolVersion("6.52.0");
      pmdExtension.setIgnoreFailures(false);
      pmdExtension.setRuleSets(emptyList());
      pmdExtension.setRuleSetFiles(project.files(
          FileUtil.copyContentToTempFile("style/pmd.xml", ".pmd.xml")
      ));
    });

    final SortedSet<String> checkstyleTaskNames = project.getTasks()
        .withType(Checkstyle.class)
        .getNames();

    final SortedSet<String> pmdTaskNames = project.getTasks()
        .withType(Pmd.class)
        .getNames();

    project.task(
        "runStaticAnalysis",
        task -> task.setDependsOn(
            Stream.concat(
                checkstyleTaskNames.stream(),
                pmdTaskNames.stream()
            ).collect(Collectors.toList())
        )
    );
  }
}
```

The most obvious and important detail is that each plugin task has to implement the Gradle `Plugin` interface.

```java
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class CodingRulesGradlePluginPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) { ... }
}
```

Then I'm configuring the Checkstyle task. I simply apply the `checkstyle` plugin, retrieve the `CheckstyleConfiguration` and override the properties I want to. Look at the code block below.

```java
project.getPluginManager().apply("checkstyle");
project.getExtensions().configure(CheckstyleExtension.class, checkstyleExtension -> {
  checkstyleExtension.setToolVersion("10.5.0");
  checkstyleExtension.setIgnoreFailures(false);
  checkstyleExtension.setMaxWarnings(0);
  checkstyleExtension.setConfigFile(
      FileUtil.copyContentToTempFile("style/checkstyle.xml", ".checkstyle.xml")
  );
});
```

The `FileUtil.copyContentToTempFile` function requires some explanation. I put the Checkstyle configuration into the `src/main/resources/style/checkstyle.xml` file. However, if you point it straightly, then people’ll get bizarre error messages on applying your Gradle in their projects. [There are some workarounds](https://stackoverflow.com/questions/20389255/reading-a-resource-file-from-within-jar) but the easiest way is to copy the content to the temporary file.

Look at the PMD configuration below. It’s similar to the Checkstyle one.

```java
project.getPluginManager().apply("pmd");
project.getExtensions().configure(PmdExtension.class, pmdExtension -> {
  pmdExtension.setConsoleOutput(true);
  pmdExtension.setToolVersion("6.52.0");
  pmdExtension.setIgnoreFailures(false);
  pmdExtension.setRuleSets(emptyList());
  pmdExtension.setRuleSetFiles(project.files(
      FileUtil.copyContentToTempFile("style/pmd.xml", ".pmd.xml")
  ));
});
```

We’re ready now. We can apply it in a real project. Though there is a slight improvement as well. Look at the code snippet below.

```java
final SortedSet<String> checkstyleTaskNames = project.getTasks()
    .withType(Checkstyle.class)
    .getNames();

final SortedSet<String> pmdTaskNames = project.getTasks()
    .withType(Pmd.class)
    .getNames();

project.task(
    "runStaticAnalysis",
    task -> task.setDependsOn(
        Stream.concat(
            checkstyleTaskNames.stream(),
            pmdTaskNames.stream()
        ).collect(Collectors.toList())
    )
);
```

The `runStaticAnalysis` task triggers all Checkstyle and PMD tasks to run sequentially. It comes in handy when you want to verify your entire project before creating a pull request. If you added the `runStaticAnalysis` task directly in the `build.gradle`, it would look like this:

```groovy
task runStaticAnalysis {
    dependsOn checkstyleMain, checkstyleTest, pmdMain, pmdTest
}
```

### Testing

What about testing? It's better to track errors during the build but not when developers have already applied your plugin in their projects. Though Gradle provides [Gradle TestKit](https://dev.to/autonomousapps/gradle-all-the-way-down-testing-your-gradle-plugin-with-gradle-testkit-2hmc) for functional testing, the case I'm showing you is rather simple and unit tests are sufficient.

Again, I'm showing the whole code piece at once and when I'll point out important details.

```java
class CodingRulesGradlePluginPluginTest {

  @Test
  void shouldApplyPluginSuccessfully() {
    final Project project = ProjectBuilder.builder().build();
    project.getPluginManager().apply("java");

    assertDoesNotThrow(
        () -> new CodingRulesGradlePluginPlugin().apply(project)
    );

    final Task task = project.getTasks().getByName("runStaticAnalysis");
    assertNotNull(task, "runStaticAnalysis task should be registered");
    final Set<String> codeStyleTasks =
        Stream.of("checkstyleMain", "checkstyleTest", "pmdTest", "pmdMain").collect(toSet());
    assertTrue(
        task.getDependsOn().containsAll(codeStyleTasks),
        format(
            "Task runStaticAnalysis should contain '%s' tasks, but actually: %s",
            codeStyleTasks,
            task.getDependsOn()
        )
    );
  }
}
```

Firstly comes test Gradle project instantiation. Look at the code snippet below.

```java
import org.gradle.testfixtures.ProjectBuilder;
import org.gradle.api.Project;

final Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply("java");
```

Gradle provides some fixtures for unit testing. The `ProjectBuilder` creates an API compatible implementation of the `Project` interface. So, you can safely pass it to the `YourPluginClass.apply` method.

Before invoking the business logic, we also manually apply the `java` plugin. Our plugin targets the Java applications. So, it’s natural to pass the Java configured `Project` implementation.

Then we simply invoke the custom plugin method and pass the configured `Project` implementation.

```java
assertDoesNotThrow(
    () -> new CodingRulesGradlePluginPlugin().apply(project)
);
```

Afterwards, comes assertions. We need to make sure that the `runStaticAnalysis` task registered successfully.

```java
final Task task = project.getTasks().getByName("runStaticAnalysis");
assertNotNull(task, "runStaticAnalysis task should be registered");
```

If it’s present, we validate the task depending on existing Checkstyle and PMD tasks.

```java
final Set<String> codeStyleTasks =
    Stream.of("checkstyleMain", "checkstyleTest", "pmdTest", "pmdMain").collect(toSet());
assertTrue(
    task.getDependsOn().containsAll(codeStyleTasks),
    format(
        "Task runStaticAnalysis should contain '%s' tasks, but actually: %s",
        codeStyleTasks,
        task.getDependsOn()
    )
);
```

That's the minimum case we should test before pushing the plugin to the https://plugins.gradle.org/.

## Releasing the plugin with GitHub Actions

When you register a new account on https://plugins.gradle.org/, go to your page and open the `API Keys` tab. You should generate new keys. There will be two of them.

```properties
gradle.publish.key=...
gradle.publish.secret=...
```

Afterwards, open the `Settings` of your repository and go to the `Secrets and Variables -> Actions` item. You have to store the obtained keys as your repository secrets.

And finally comes the GitHub Actions build configuration.

> I placed mine in `.github/workflow/build.yml` file.

Look at the whole setup below. Then I'm telling you the meanings of particular blocks.

```yaml
name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 8
        uses: actions/setup-java@v3
        with:
          java-version: '8'
          distribution: 'temurin'
      - name: Build with Gradle
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build
  publish:
    needs:
      - build
    if: github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    steps:
      - name: Auto Increment Semver Action
        uses: MCKanpolat/auto-semver-action@1.0.5
        id: versioning
        with:
          releaseType: minor
          incrementPerCommit: false
          github_token: ${{ secrets.GITHUB_TOKEN }}
      - name: Next Release Number
        run: echo ${{ steps.versioning.outputs.version }}
      - uses: actions/checkout@v3
      - name: Set up JDK 8
        uses: actions/setup-java@v3
        with:
          java-version: '8'
          distribution: 'temurin'
      - name: Publish Gradle plugin
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build publishPlugins -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} -Pversion=${{ steps.versioning.outputs.version }}
```

The top file declaration states the rules of pipeline triggering.

```java
name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]
```

The pipeline runs on each pull request to `master` branch and every building of `master` branch itself.

The build consists of two jobs. The first one is trivial. It just runs the Gradle `build` task. Look at the configuration below.

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 8
        uses: actions/setup-java@v3
        with:
          java-version: '8'
          distribution: 'temurin'
      - name: Build with Gradle
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build
```

And then comes the publishing itself. It also contains several steps. The first increments the version automatically and saves it to the environment variable. It’s pretty convenient, because Gradle plugins cannot be published as snapshots.

```yaml
publish:
    needs:
      - build
    if: github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    steps:
      - name: Auto Increment Semver Action
        uses: MCKanpolat/auto-semver-action@1.0.5
        id: versioning
        with:
          releaseType: minor
          incrementPerCommit: false
          github_token: ${{ secrets.GITHUB_TOKEN }}
      - name: Next Release Number
        run: echo ${{ steps.versioning.outputs.version }}
```

> The `if: github.ref == ‘refs/heads/master’` tells GitHub Actions worker to run the pipeline only if `master` branch is being built. Therefore, GitHub Actions won’t trigger the `publish` process during the pull request build.

And now we need to publish the packed plugin itself. Look at the code snippet below.

```yaml
      - uses: actions/checkout@v3
      - name: Set up JDK 8
        uses: actions/setup-java@v3
        with:
          java-version: '8'
          distribution: 'temurin'
      - name: Publish Gradle plugin
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build publishPlugins -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} -Pversion=${{ steps.versioning.outputs.version }}
```

As you can see, GitHub Actions passes the `gradle.publish.key` and the `gradle.publish.secret` properties through the secrets and the new project version as the environment variable.

## Conclusion

As you can see, automating code style rules checking is not that complicated in Gradle. By the way, you can apply the plugin described in the project by including `id 'io.github.simonharmonicminor.code.style' version '0.1.0'`.

If you have any questions or suggestions, leave your comments down below. Thanks for reading!

## Resources

1. [My article "Why You Need Static Code Analysis"](https://dev.to/kirekov/why-you-need-static-code-analysis-28po)
1. [plugins.gradle.org](https://plugins.gradle.org/)
1. [The repository with source code](https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example)
1. [PMD](https://pmd.github.io/)
1. [Checkstyle](https://checkstyle.sourceforge.io/)
1. [Monorepository](https://monorepo.tools/)
1. [Polyrepository](https://earthly.dev/blog/monorepo-vs-polyrepo/)
1. [Copy and paste programming](https://en.wikipedia.org/wiki/Copy-and-paste_programming)
1. [Apache Maven naming conventions](https://maven.apache.org/guides/mini/guide-naming-conventions.html)
1. [Reading a resource file from within jar](https://stackoverflow.com/questions/20389255/reading-a-resource-file-from-within-jar)
1. [Gradle TestKit](https://dev.to/autonomousapps/gradle-all-the-way-down-testing-your-gradle-plugin-with-gradle-testkit-2hmc)