Setting up linters in Gitlab CI for C++ and Groovy / Jenkins code

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

Logos Gitlab-CI, CodeNarc & clang-tidy

Linting C++ code with clang-tidy

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.

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:

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 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 analyzes Groovy code for defects, bad practices, inconsistencies, style issues and more.

It can be used to perform static code analysis on Jenkins pipelines.

Because executing CodeNarc from the command-line is not so simple, I find it easier to use Gradle and its dedicated plugin to execute CodeNarc:

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:

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

codenarc_rules.groovy
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')

}

The details for all rules can be found there: codenarc.org/codenarc-rule-index.html