Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
rico-chet opened this issue Nov 13, 2024 · 2 comments
Open

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

rico-chet opened this issue Nov 13, 2024 · 2 comments

Comments

@rico-chet
Copy link
Contributor

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
Copy link
Contributor Author

rico-chet commented Nov 13, 2024

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

@bdbaddog
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants