DEV Community

Semyon Kirekov
Semyon Kirekov

Posted on

Custom Gradle Plugin for Unified Static Code Analysis

Static code analysis 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

So, in this article I'm showing you:

  1. How you can create the Gradle plugin with custom PMD and Checkstyle rules.
  2. How to publish it to plugins.gradle.org.
  3. How to automate the releasing process with GitHub Actions.

You can check out the code examples in this repository.

PMD, Checkstyle, and difficulties with polyrepos

PMD and Checkstyle are static analysis tools that check your code on each project build. Gradle allows to apply them easily.

plugins {
    id 'java'
    id 'pmd'
    id 'checkstyle'
}
Enter fullscreen mode Exit fullscreen mode

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

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

pmd {
    consoleOutput = true
    toolVersion = '6.52.0'
    ignoreFailures = false
    ruleSetFiles = file(pathToPmdConfig)
}
Enter fullscreen mode Exit fullscreen mode

If your entire project (or even company) is the monorepository, 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? 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 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.

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()
}
Enter fullscreen mode Exit fullscreen mode

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

plugins {
    id 'java-gradle-plugin'
    id 'com.gradle.plugin-publish' version '1.1.0'
}
Enter fullscreen mode Exit fullscreen mode

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.

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

repositories {
    mavenCentral()
}
Enter fullscreen mode Exit fullscreen mode

The group defines groupId in favor of Apache Maven naming conventions. 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.

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'
}
Enter fullscreen mode Exit fullscreen mode

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

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'])
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

tasks.named('test') {
    useJUnitPlatform()
}
Enter fullscreen mode Exit fullscreen mode

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.

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())
        )
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

public class CodingRulesGradlePluginPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) { ... }
}
Enter fullscreen mode Exit fullscreen mode

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.

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")
  );
});
Enter fullscreen mode Exit fullscreen mode

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 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.

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")
  ));
});
Enter fullscreen mode Exit fullscreen mode

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.

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())
    )
);
Enter fullscreen mode Exit fullscreen mode

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:

task runStaticAnalysis {
    dependsOn checkstyleMain, checkstyleTest, pmdMain, pmdTest
}
Enter fullscreen mode Exit fullscreen mode

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 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.

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()
        )
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

final Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply("java");
Enter fullscreen mode Exit fullscreen mode

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.

assertDoesNotThrow(
    () -> new CodingRulesGradlePluginPlugin().apply(project)
);
Enter fullscreen mode Exit fullscreen mode

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

final Task task = project.getTasks().getByName("runStaticAnalysis");
assertNotNull(task, "runStaticAnalysis task should be registered");
Enter fullscreen mode Exit fullscreen mode

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

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()
    )
);
Enter fullscreen mode Exit fullscreen mode

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.

gradle.publish.key=...
gradle.publish.secret=...
Enter fullscreen mode Exit fullscreen mode

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.

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 }}
Enter fullscreen mode Exit fullscreen mode

The top file declaration states the rules of pipeline triggering.

name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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 }}
Enter fullscreen mode Exit fullscreen mode

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.

      - 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 }}
Enter fullscreen mode Exit fullscreen mode

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"
  2. plugins.gradle.org
  3. The repository with source code
  4. PMD
  5. Checkstyle
  6. Monorepository
  7. Polyrepository
  8. Copy and paste programming
  9. Apache Maven naming conventions
  10. Reading a resource file from within jar
  11. Gradle TestKit

Top comments (1)

Collapse
 
Sloan, the sloth mascot
Comment deleted