Setting up linters in Gitlab CI for C++ and Groovy / Jenkins code
Tags: groovy, cpp, linter, clang
Published: 2024-05-03 11:42:05

A very short blog post to share some minimal code snippets on how to quickly and easily setup static code analysis tools on C++ code and Jenkins pipelines (or any Groovy code), for example in Gitlab CI pipelines.

![Logos Gitlab-CI, CodeNarc & clang-tidy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5xy6qs3m0zu4hcigll6.png)

## Linting C++ code with clang-tidy

> [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) is a clang-based C++ linter tool that can identify and auto-fix some programming errors, like style violations, interface misuse, or bugs that can be deduced via static analysis.

```yaml
cpp-linter:
  # No need to specify the next line if your default Docker image is already an alpine:
  image: alpine:3.17
  script:
    - apk add clang-extra-tools
    - find . -name '*.cpp' -exec clang-tidy --fix {} \; | tee clang-tidy.log
    # Fail the job if errors were found:
    - ! grep "Found compiler error" clang-tidy.log
  rules:
    # Executing for every commit on the main branch where C++ files were modified:
    - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"
      changes:
        - "*.cpp"
        - "**/*.cpp"
    # Executing for every commit in a non-draft merge request where C++ files were modified
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE !~ /^Draft:.*$/
      changes:
        - "*.cpp"
        - "**/*.cpp"
```

You should also initiate a `.clang-tidy` configuration file like this:
```yaml
Checks: '*,-llvmlibc-*'
WarningsAsErrors: '*'
```

This default file is a strict starting point: it enables all rules, except the ones specific to LLVM, and treat all warnings as errors.
You can then add `-${ruleName}` to the `Checks` entry in this file to disable some checks.
Check the [clang-tidy documentation](https://clang.llvm.org/extra/clang-tidy/) for more details about rules and suppressing errors & warnings using code comments.

You can also generate an exhaustive `.clang-tidy` configuration file, with an extra `CheckOptions` field listing all default values for rules parameters, by running this command:
```
clang-tidy -checks='*,-llvmlibc-*' -warnings-as-errors='*' -dump-config > .clang-tidy
```


## Linting Groovy / Jenkins code with CodeNarc

> [CodeNarc](https://codenarc.org/) analyzes Groovy code for defects, bad practices, inconsistencies, style issues and more.

It can be used to perform static code analysis on [Jenkins pipelines](https://www.jenkins.io/doc/book/pipeline/).

Because [executing CodeNarc from the command-line](https://codenarc.org/codenarc-command-line.html#executing-codenarc-from-the-command-line) is not so simple, I find it easier to use [Gradle](https://gradle.org/) and its dedicated plugin to execute CodeNarc:

```yaml
groovy-linter:
  image: gradle:8.7
  script:
    - gradle check
  rules:
    # Executing for every commit on the main branch where C++ files were modified:
    - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"
      changes:
        - "*.groovy"
        - "**/*.groovy"
    # Executing for every commit in a non-draft merge request where Groovy files were modified
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE !~ /^Draft:.*$/
      changes:
        - "*.groovy"
        - "**/*.groovy"
```

You will also need a `build.gradle` file in the directory where the `gradle` command is executed:
```groovy
plugins {
    id 'groovy'
    id 'codenarc'
}
repositories {
    jcenter()
    // Required if you have Jenkins dependencies:
    maven {
        url('https://repo.jenkins-ci.org/public/')
    }
}
def codeNarcVersion = '3.4.0'
dependencies {
    // CodeNarc must be added there, or you will get a ClassNotFoundException:
    codenarc group: 'org.codenarc', name: 'CodeNarc', version: codeNarcVersion
    codenarc group: 'org.jenkins-ci.plugins', name: 'plugin', version: '4.79'
}
sourceSets {
    main {
        groovy {
            // Specify the relative paths to the directories containing your .groovy files:
            srcDirs = ['.']
        }
    }
}
codenarc {
    configFile = file('./codenarc_rules.groovy')
    toolVersion = codeNarcVersion
    reportFormat = 'text'
}
// The two following blocks allow to display the CodeNarc report in the console:
// ( this recipe comes from this SO answer: https://stackoverflow.com/a/36899862/636849 )
task codenarcConsoleReport {
    doLast {
        println file("${codenarc.reportsDir}/main.txt").text
    }
}
codenarcMain {
    finalizedBy codenarcConsoleReport
    compilationClasspath = sourceSets.main.compileClasspath + sourceSets.main.output //+ files('../relative/path/to/shared/lib/dir')
}
// You can optionnally disable the Groovy compilation step, to execute faster:
compileGroovy.enabled = false
```

And finally you can initialize a configuration file for the linter, named `codenarc_rules.groovy`:

<details>
  <summary><em>codenarc_rules.groovy</em></summary>
  <pre><code>ruleset {
    ruleset('rulesets/basic.xml')
    ruleset('rulesets/braces.xml')
    ruleset('rulesets/comments.xml')
    ruleset('rulesets/concurrency.xml')
    ruleset('rulesets/convention.xml') {
        // You may want to disable some rules, like this:
        CompileStatic(enabled:false)
        MethodParameterTypeRequired(enabled:false)
        MethodReturnTypeRequired(enabled:false)
        NoDef(enabled:false)
        PublicMethodsBeforeNonPublicMethods(enabled:false)
        VariableTypeRequired(enabled:false)
        TrailingComma(enabled:false)
    }
    ruleset('rulesets/design.xml')
    ruleset('rulesets/dry.xml') {
        // Allowing several benign cases of code duplication:
        DuplicateListLiteral(enabled:false)
        DuplicateMapLiteral(enabled:false)
        DuplicateNumberLiteral(enabled:false)
        DuplicateStringLiteral(enabled:false)
    }
    ruleset('rulesets/enhanced.xml')
    ruleset('rulesets/exceptions.xml')
    ruleset('rulesets/formatting.xml') {
        // Disabling rules that are too strict for my project:
        BlockEndsWithBlankLine(enabled:false)
        BlockStartsWithBlankLine(enabled:false)
        ConsecutiveBlankLines(enabled:false)
        Indentation(enabled:false)
        LineLength(enabled:false)
        SpaceAfterOpeningBrace(enabled:false)
        SpaceAroundMapEntryColon(enabled:false)
        SpaceBeforeClosingBrace(enabled:false)
    }
    ruleset('rulesets/generic.xml')
    // ruleset('rulesets/grails.xml')
    ruleset('rulesets/groovyism.xml')
    ruleset('rulesets/imports.xml')
    // ruleset('rulesets/jdbc.xml')
    // ruleset('rulesets/junit.xml')
    ruleset('rulesets/logging.xml') {
        // Allowing to print to stderr & stdout
        SystemErrPrint(enabled:false)
        SystemOutPrint(enabled:false)
    }
    ruleset('rulesets/naming.xml')
    ruleset('rulesets/security.xml')
    ruleset('rulesets/serialization.xml')
    ruleset('rulesets/size.xml')
    ruleset('rulesets/unnecessary.xml')
    ruleset('rulesets/unused.xml')
}</code></pre>
</details>

The details for all rules can be found there: [codenarc.org/codenarc-rule-index.html](https://codenarc.org/codenarc-rule-index.html)

Original post on my blog: https://chezsoi.org/lucas/blog/setting-up-linters-in-gitlab-ci-for-c-and-groovy-jenkins-code.html