scons icon indicating copy to clipboard operation
scons copied to clipboard

Documentation should provide an example of C++ static code analysis

Open rico-chet opened this issue 1 year ago • 2 comments

C++ is a complex beast, and there is a number of static code analysis (SCA) tools which aim to help developers tame this complexity. The tools are run best as part of the build, but they can be slow.

Documentation should provide an example of running a set of SCA tools as part of the build. It can show SCons' abilities of automatic dependency tracking and caching which enable users to run precise SCA scans as part of the inner development loop without sacrificing development performance.

rico-chet avatar Nov 13 '24 15:11 rico-chet

Here's an example I came up with:

#!/usr/bin/env python3

from SCons.Script import *
from pathlib import Path
from subprocess import run, DEVNULL, PIPE

env = Environment()

env.Tool("compilation_db")
compilation_db = env.CompilationDatabase()[0]

cpp_sources = Split(
    """
	main.cpp
	another.cpp
"""
)
program = env.Program("prog", cpp_sources)


AddOption(
    "--no-clang-tidy",
    action="store_true",
    help="Don't run clang-tidy static code analysis automatically",
)

AddOption(
    "--no-clazy",
    action="store_true",
    help="Don't run clazy static code analysis automatically",
)

AddOption(
    "--no-cppcheck",
    action="store_true",
    help="Don't run cppcheck static code analysis automatically",
)


def run_clang_tidy(target, source, env):
    cpp_source = str(source[0])
    compilation_db = Path(str(source[1]))

    result = run(
        [
            "clang-tidy",
            "-p",
            compilation_db.parent,
            "--checks=readability-*",
            cpp_source,
        ],
        env=env["ENV"],
        stdout=PIPE,
        stderr=DEVNULL,
        text=True,
    )

    with open(str(target[0]), "w") as target_fh:
        target_fh.write(result.stdout)

    good = result.returncode == 0 and not result.stdout
    if not good:
        print(result.stdout)
        return 1

    return 0


def run_clazy(target, source, env):
    cpp_source = str(source[0])
    compilation_db = Path(str(source[1]))

    result = run(
        ["clazy-standalone", "-p", compilation_db.parent, cpp_source],
        env={**env["ENV"], "CLAZY_CHECKS": "level2"},
        stdout=PIPE,
        stderr=PIPE,
        text=True,
    )

    with open(str(target[0]), "w") as target_fh:
        target_fh.write(result.stdout)
        target_fh.write(result.stderr)

    good = result.returncode == 0 and not result.stderr
    if not good:
        print(result.stderr)
        return 1

    return 0


def run_cppcheck(target, source, env):
    result = run(
        ["cppcheck", str(source[0])],
        env=env["ENV"],
        stdout=DEVNULL,
        stderr=PIPE,
        text=True,
    )

    with open(str(target[0]), "w") as target_fh:
        target_fh.write(result.stderr)

    good = result.returncode == 0 and not result.stderr
    if not good:
        print(result.stderr)
        return 1

    return 0


clang_tidy_reports = [
    env.Command(
        source=[cpp_source, compilation_db],
        action=Action(run_clang_tidy, cmdstr="Running clang-tidy on ${SOURCE}"),
        target=f"{Path(cpp_source).stem}.clang-tidy.log",
    )
    for cpp_source in cpp_sources
]


clazy_reports = [
    env.Command(
        source=[cpp_source, compilation_db],
        action=Action(run_clazy, cmdstr="Running clazy on ${SOURCE}"),
        target=f"{Path(cpp_source).stem}.clazy.log",
    )
    for cpp_source in cpp_sources
]


cppcheck_reports = [
    env.Command(
        source=cpp_source,
        action=Action(run_cppcheck, cmdstr="Running cppcheck on ${SOURCE}"),
        target=f"{Path(cpp_source).stem}.cppcheck.log",
    )
    for cpp_source in cpp_sources
]

env.Default(
    [program]
    + ([] if GetOption("no_clang_tidy") else clang_tidy_reports)
    + ([] if GetOption("no_cppcheck") else cppcheck_reports)
    + ([] if GetOption("no_clazy") else clazy_reports)
)

It takes ~2.2 seconds clean on a single core, and ~0.16 seconds when C++/SCons files are unchanged. Most notably, it only takes ~0.7 seconds when one of the two C++ files changed, due to parallelization and caching. Header dependencies are tracked, and a change is detected and leads to a precisely minimal run of the SCA tools.

Feel free to take the idea further, I am not familiar with the docbook format (I like AsciiDoc very much, though ;-)).

rico-chet avatar Nov 13 '24 15:11 rico-chet

This is not something we'd put in the users guide, but it should fit in the cook book.. https://github.com/SCons/cookbook Feel free to make a PR against that.

bdbaddog avatar Nov 14 '24 18:11 bdbaddog