pvs-studio: Add support for linting with PVS-Studio

This commit is contained in:
Martin Duffy
2025-10-17 15:27:20 -04:00
parent 8f722e0fac
commit ef12ef1bd6
26 changed files with 321 additions and 16 deletions

View File

@@ -327,6 +327,7 @@ Properties on Targets
/prop_tgt/LANG_ICSTAT
/prop_tgt/LANG_INCLUDE_WHAT_YOU_USE
/prop_tgt/LANG_LINKER_LAUNCHER
/prop_tgt/LANG_PVS_STUDIO
/prop_tgt/LANG_STANDARD
/prop_tgt/LANG_STANDARD_REQUIRED
/prop_tgt/LANG_VISIBILITY_PRESET

View File

@@ -529,6 +529,7 @@ Variables that Control the Build
/variable/CMAKE_LANG_LINK_LIBRARY_USING_FEATURE_SUPPORTED
/variable/CMAKE_LANG_LINK_WHAT_YOU_USE_FLAG
/variable/CMAKE_LANG_LINKER_LAUNCHER
/variable/CMAKE_LANG_PVS_STUDIO
/variable/CMAKE_LANG_USING_LINKER_TYPE
/variable/CMAKE_LANG_VISIBILITY_PRESET
/variable/CMAKE_LIBRARY_OUTPUT_DIRECTORY

View File

@@ -6,11 +6,12 @@ SKIP_LINTING
This property allows you to exclude a specific source file
from the linting process. The linting process involves running
tools such as :prop_tgt:`<LANG>_CPPLINT`, :prop_tgt:`<LANG>_CLANG_TIDY`,
:prop_tgt:`<LANG>_CPPCHECK`, :prop_tgt:`<LANG>_ICSTAT` and
:prop_tgt:`<LANG>_INCLUDE_WHAT_YOU_USE` on the source files, as well
as compiling header files as part of :prop_tgt:`VERIFY_INTERFACE_HEADER_SETS`.
By setting ``SKIP_LINTING`` on a source file, the mentioned linting tools
will not be executed for that particular file.
:prop_tgt:`<LANG>_CPPCHECK`, :prop_tgt:`<LANG>_ICSTAT`,
:prop_tgt:`<LANG>_PVS_STUDIO` and :prop_tgt:`<LANG>_INCLUDE_WHAT_YOU_USE` on the
source files, as well as compiling header files as part of
:prop_tgt:`VERIFY_INTERFACE_HEADER_SETS`. By setting ``SKIP_LINTING`` on a
source file, the mentioned linting tools will not be executed for that
particular file.
Example
^^^^^^^

View File

@@ -0,0 +1,39 @@
<LANG>_PVS_STUDIO
-----------------
.. versionadded:: 4.3
This property is implemented only when ``<LANG>`` is ``C`` or ``CXX``.
Specify a :ref:`semicolon-separated list <CMake Language Lists>` containing
a command line for the ``pvs-studio-analyzer`` tool (named
``CompilerCommandsAnalyzer`` on Windows). The :ref:`Makefile Generators` and
:ref:`Ninja Generators` will run this tool along with the compiler and
report a warning if the tool reports any problems.
The specified ``pvs-studio-analyzer`` command line will be invoked with
the following additional arguments:
- ``--source-file``: The source file.
- ``--output-file``: A path adjacent to the object file to write the PVS log.
- ``--cl-params``: The compile options.
- ``--preprocessor``: The preprocessor, based on
:variable:`CMAKE_<LANG>_COMPILER_ID`, if determined to be one of:
``visualcpp``, ``clang``, ``gcc``, ``bcc``, ``iar``.
- ``--platform``: The target platform, if determined to be one of: ``arm``,
``win32``, ``x64``, ``linux32``, ``linux64``, ``macOS``.
See the
`PVS-Studio documentation <https://pvs-studio.com/en/docs/manual/6615/#flags>`_
for details on these and other available options.
CMake will look for the ``plog-converter`` tool in the same directory as the
provided ``pvs-studio-analyzer``, and in the user's path if not present in that
directory. The ``plog-converter`` will run automatically with the ``Txt``
output type on Windows, and ``errorfile`` on other platforms, and the contents
of that file will be sent to ``stderr``. The PVS log file will be deleted after
the converter runs.
This property is initialized by the value of
the :variable:`CMAKE_<LANG>_PVS_STUDIO` variable if it is set
when a target is created.

View File

@@ -0,0 +1,7 @@
pvs-analyze
-----------
* A :prop_tgt:`<LANG>_PVS_STUDIO` target property and supporting
:variable:`CMAKE_<LANG>_PVS_STUDIO` variable were introduced to tell
:ref:`Makefile Generators` and :ref:`Ninja Generators` to run
``pvs-studio-analyzer`` with the compiler for ``C`` and ``CXX`` languages.

View File

@@ -0,0 +1,15 @@
CMAKE_<LANG>_PVS_STUDIO
-----------------------
.. versionadded:: 4.3
Default value for :prop_tgt:`<LANG>_PVS_STUDIO` target property
when ``<LANG>`` is ``C`` or ``CXX``.
This variable is used to initialize the property on each target as it is
created. For example:
.. code-block:: cmake
set(CMAKE_CXX_PVS_STUDIO pvs-studio-analyzer analyze -a "GA\;OP")
add_executable(foo foo.cxx)

View File

@@ -360,6 +360,7 @@ std::string cmCommonTargetGenerator::GenerateCodeCheckRules(
std::string cpplint;
std::string cppcheck;
std::string icstat;
std::string pvs;
auto evaluateProp = [&](std::string const& prop) -> std::string {
auto const value = this->GeneratorTarget->GetProperty(prop);
@@ -386,9 +387,13 @@ std::string cmCommonTargetGenerator::GenerateCodeCheckRules(
std::string const icstat_prop = cmStrCat(lang, "_ICSTAT");
icstat = evaluateProp(icstat_prop);
std::string const pvs_prop = cmStrCat(lang, "_PVS_STUDIO");
pvs = evaluateProp(pvs_prop);
}
if (cmNonempty(iwyu) || cmNonempty(tidy) || cmNonempty(cpplint) ||
cmNonempty(cppcheck) || cmNonempty(icstat)) {
cmNonempty(cppcheck) || cmNonempty(icstat) || cmNonempty(pvs)) {
std::string code_check = cmakeCmd + " -E __run_co_compile";
if (!compilerLauncher.empty()) {
// In __run_co_compile case the launcher command is supplied
@@ -476,6 +481,29 @@ std::string cmCommonTargetGenerator::GenerateCodeCheckRules(
exportFixes));
}
}
if (cmNonempty(pvs)) {
cmMakefile* mf =
this->GeneratorTarget->GetLocalGenerator()->GetMakefile();
std::string extraPvsArgs;
if (lang == "CXX") {
extraPvsArgs +=
cmStrCat(";--cxx;", mf->GetDefinition("CMAKE_CXX_COMPILER"));
} else if (lang == "C") {
extraPvsArgs +=
cmStrCat(";--cc;", mf->GetDefinition("CMAKE_C_COMPILER"));
}
// cocompile args
code_check += " --pvs-studio=";
code_check += this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(
cmStrCat(pvs, extraPvsArgs));
code_check += " --object=";
code_check +=
this->GeneratorTarget->GetLocalGenerator()->ConvertToOutputFormat(
cmSystemTools::CollapseFullPath(
cmStrCat(this->GeneratorTarget->GetObjectDirectory(config), '/',
this->GeneratorTarget->GetObjectName(&source))),
cmOutputConverter::SHELL);
}
if (cmNonempty(cpplint)) {
code_check += " --cpplint=";
code_check +=
@@ -504,7 +532,8 @@ std::string cmCommonTargetGenerator::GenerateCodeCheckRules(
code_check += this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(
cmStrCat(icstat, checksParam, dbParam));
}
if (cmNonempty(tidy) || (cmNonempty(cpplint)) || (cmNonempty(cppcheck))) {
if (cmNonempty(tidy) || (cmNonempty(cpplint)) || (cmNonempty(cppcheck)) ||
cmNonempty(pvs)) {
code_check += " --source=";
code_check +=
this->GeneratorTarget->GetLocalGenerator()->ConvertToOutputFormat(

View File

@@ -473,6 +473,7 @@ TargetProperty const StaticTargetProperties[] = {
{ "C_CPPCHECK"_s, IC::CanCompileSources },
{ "C_ICSTAT"_s, IC::CanCompileSources },
{ "C_INCLUDE_WHAT_YOU_USE"_s, IC::CanCompileSources },
{ "C_PVS_STUDIO"_s, IC::CanCompileSources },
// -- C++
{ "CXX_CLANG_TIDY"_s, IC::CanCompileSources },
{ "CXX_CLANG_TIDY_EXPORT_FIXES_DIR"_s, IC::CanCompileSources },
@@ -480,6 +481,7 @@ TargetProperty const StaticTargetProperties[] = {
{ "CXX_CPPCHECK"_s, IC::CanCompileSources },
{ "CXX_ICSTAT"_s, IC::CanCompileSources },
{ "CXX_INCLUDE_WHAT_YOU_USE"_s, IC::CanCompileSources },
{ "CXX_PVS_STUDIO"_s, IC::CanCompileSources },
// -- Objective C
{ "OBJC_CLANG_TIDY"_s, IC::CanCompileSources },
{ "OBJC_CLANG_TIDY_EXPORT_FIXES_DIR"_s, IC::CanCompileSources },
@@ -1808,6 +1810,7 @@ void cmTarget::CopyImportedCxxModulesProperties(cmTarget const* tgt)
"CXX_CPPCHECK",
"CXX_ICSTAT",
"CXX_INCLUDE_WHAT_YOU_USE",
"CXX_PVS_STUDIO",
"SKIP_LINTING",
// Build graph properties

View File

@@ -411,6 +411,81 @@ int HandleTidy(std::string const& runCmd, std::string const& sourceFile,
return ret;
}
int HandlePVSStudio(std::string const& runCmd, std::string const& sourceFile,
std::string const& objectFile,
std::vector<std::string> const& orig_cmd)
{
cmList pvsCmd{ runCmd, cmList::EmptyElements::Yes };
std::string logFile = cmStrCat(objectFile, "-pvs.log");
std::string errFile = cmStrCat(objectFile, "-pvs.err");
pvsCmd.reserve(pvsCmd.size() + 5 + orig_cmd.size());
pvsCmd.emplace_back("--source-file");
pvsCmd.emplace_back(sourceFile);
pvsCmd.emplace_back("--output-file");
pvsCmd.emplace_back(logFile);
pvsCmd.emplace_back("--cl-params");
for (size_t i = 1; i < orig_cmd.size(); ++i) {
pvsCmd.emplace_back(orig_cmd[i]);
}
int ret;
std::string stdOut;
std::string stdErr;
// Run the PVS command line. Capture its stdout and hide its stderr.
if (!cmSystemTools::RunSingleCommand(pvsCmd, &stdOut, &stdErr, &ret, nullptr,
cmSystemTools::OUTPUT_NONE)) {
std::cerr << "Error running '" << pvsCmd[0] << "': " << stdErr << '\n';
return 1;
}
if (ret != 0) {
if (ret == 7 && !cmSystemTools::FileExists(logFile)) {
return 0; // Analyzer generated no output from source
}
std::cout << stdOut;
std::cerr << stdErr;
return ret;
}
// Find the plog-converter tool
#ifdef _WIN32
std::string plogConvertName = "HtmlGenerator.exe";
#else
std::string plogConvertName = "plog-converter";
#endif
std::string plogConvert = cmStrCat(
cmSystemTools::GetFilenamePath(pvsCmd.front()), '/', plogConvertName);
if (!cmSystemTools::FileIsExecutable(plogConvert)) {
plogConvert = cmSystemTools::FindProgram(plogConvertName);
}
if (plogConvert.empty()) {
std::cerr << "Could not find " << plogConvertName << std::endl;
return 1;
}
// Run the plog-converter tool
std::vector<std::string> plogCmd{ plogConvert, "-t", "errorfile",
"-o", errFile, logFile };
if (!cmSystemTools::RunSingleCommand(plogCmd, &stdOut, &stdErr, &ret,
nullptr, cmSystemTools::OUTPUT_NONE)) {
std::cerr << "Error running '" << plogCmd[0] << "': " << stdErr << '\n';
return 1;
}
// Show error messages from plog-converter output
if (stdOut.find("Total messages 0") == std::string::npos) {
cmsys::ifstream errFileStream(errFile.c_str());
// output always begins with a Help message
errFileStream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cerr << errFileStream.rdbuf();
}
cmSystemTools::RemoveFile(logFile);
if (ret != 0) {
std::cerr << stdErr;
}
return ret;
}
int HandleLWYU(std::string const& runCmd, std::string const& sourceFile,
std::string const& /*objectFile*/,
std::vector<std::string> const&)
@@ -585,15 +660,16 @@ struct CoCompiler
bool NoOriginalCommand;
};
std::array<CoCompiler, 6> const CoCompilers = {
{ // Table of options and handlers.
{ "--cppcheck=", HandleCppCheck, false },
{ "--cpplint=", HandleCppLint, false },
{ "--icstat=", HandleIcstat, false },
{ "--iwyu=", HandleIWYU, false },
{ "--lwyu=", HandleLWYU, true },
{ "--tidy=", HandleTidy, false } }
};
std::array<CoCompiler, 7> const CoCompilers = { {
// Table of options and handlers.
{ "--cppcheck=", HandleCppCheck, false },
{ "--cpplint=", HandleCppLint, false },
{ "--icstat=", HandleIcstat, false },
{ "--iwyu=", HandleIWYU, false },
{ "--lwyu=", HandleLWYU, true },
{ "--pvs-studio=", HandlePVSStudio, false },
{ "--tidy=", HandleTidy, false },
} };
struct CoCompileJob
{

View File

@@ -310,6 +310,7 @@ if(CMake_TEST_Qt6 AND Qt6Widgets_FOUND)
"-DCMAKE_PREFIX_PATH:STRING=${base_dir}"
-DPSEUDO_TIDY=$<TARGET_FILE:pseudo_tidy>
-DPSEUDO_IWYU=$<TARGET_FILE:pseudo_iwyu>
-DPSEUDO_PVS=$<TARGET_FILE:pseudo_pvs>
-DPSEUDO_CPPLINT=$<TARGET_FILE:pseudo_cpplint>
-DPSEUDO_CPPCHECK=$<TARGET_FILE:pseudo_cppcheck>
)
@@ -1166,6 +1167,13 @@ add_executable(pseudo_tidy pseudo_tidy.c)
add_executable(pseudo_iwyu pseudo_iwyu.c)
add_executable(pseudo_cpplint pseudo_cpplint.c)
add_executable(pseudo_cppcheck pseudo_cppcheck.c)
add_executable(pseudo_pvs pseudo_pvs.c)
add_executable(pseudo_plog_converter pseudo_plog_converter.c)
if (WIN32)
set_target_properties(pseudo_plog_converter PROPERTIES OUTPUT_NAME HtmlGenerator)
else()
set_target_properties(pseudo_plog_converter PROPERTIES OUTPUT_NAME plog-converter)
endif()
if("${CMAKE_GENERATOR}" MATCHES "Make|Ninja|FASTBuild")
if(UNIX AND NOT CYGWIN)
@@ -1179,11 +1187,13 @@ if("${CMAKE_GENERATOR}" MATCHES "Make|Ninja|FASTBuild")
endif()
add_RunCMake_test(ClangTidy -DPSEUDO_TIDY=$<TARGET_FILE:pseudo_tidy>)
add_RunCMake_test(IncludeWhatYouUse -DPSEUDO_IWYU=$<TARGET_FILE:pseudo_iwyu>)
add_RunCMake_test(PVSStudio -DPSEUDO_PVS=$<TARGET_FILE:pseudo_pvs>)
add_RunCMake_test(Cpplint -DPSEUDO_CPPLINT=$<TARGET_FILE:pseudo_cpplint>)
add_RunCMake_test(Cppcheck -DPSEUDO_CPPCHECK=$<TARGET_FILE:pseudo_cppcheck>)
add_RunCMake_test(MultiLint
-DPSEUDO_TIDY=$<TARGET_FILE:pseudo_tidy>
-DPSEUDO_IWYU=$<TARGET_FILE:pseudo_iwyu>
-DPSEUDO_PVS=$<TARGET_FILE:pseudo_pvs>
-DPSEUDO_CPPLINT=$<TARGET_FILE:pseudo_cpplint>
-DPSEUDO_CPPCHECK=$<TARGET_FILE:pseudo_cppcheck>
)

View File

@@ -4,4 +4,5 @@
--icstat=
--iwyu=
--lwyu=
--pvs-studio=
--tidy=

View File

@@ -3,4 +3,5 @@ set(CMAKE_C_INCLUDE_WHAT_YOU_USE "${PSEUDO_IWYU}" -some -args)
set(CMAKE_C_CLANG_TIDY "${PSEUDO_TIDY}" -some -args)
set(CMAKE_C_CPPLINT "${PSEUDO_CPPLINT}" --verbose=0 --linelength=80)
set(CMAKE_C_CPPCHECK "${PSEUDO_CPPCHECK}")
set(CMAKE_C_PVS_STUDIO "${PSEUDO_PVS}" analyze)
add_executable(main main.c)

View File

@@ -3,4 +3,5 @@ set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE "$<1:${PSEUDO_IWYU}>" -some -args)
set(CMAKE_CXX_CLANG_TIDY "$<1:${PSEUDO_TIDY}>" -some -args)
set(CMAKE_CXX_CPPLINT "$<1:${PSEUDO_CPPLINT}>" --verbose=0 --linelength=80)
set(CMAKE_CXX_CPPCHECK "$<1:${PSEUDO_CPPCHECK}>")
set(CMAKE_CXX_PVS_STUDIO "$<1:${PSEUDO_PVS}>" analyze)
add_executable(main main.cxx)

View File

@@ -5,6 +5,7 @@ set(RunCMake_TEST_OPTIONS
"-DPSEUDO_CPPLINT=${PSEUDO_CPPLINT}"
"-DPSEUDO_IWYU=${PSEUDO_IWYU}"
"-DPSEUDO_TIDY=${PSEUDO_TIDY}"
"-DPSEUDO_PVS=${PSEUDO_PVS}"
)
function(run_multilint lang)

View File

@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 4.1)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)

View File

@@ -0,0 +1,17 @@
include(RunCMake)
set(RunCMake_TEST_OPTIONS "-DPSEUDO_PVS=${PSEUDO_PVS}")
function(run_pvs test)
# Use a single build tree for tests without cleaning.
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test})
set(RunCMake_TEST_NO_CLEAN 1)
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
run_cmake(${test})
set(RunCMake_TEST_OUTPUT_MERGE 1)
run_cmake_command(${test}-build ${CMAKE_COMMAND} --build .)
endfunction()
run_pvs(pvs)
run_pvs(pvs-bad-arg)

View File

@@ -0,0 +1,4 @@
int main()
{
return 0;
}

View File

@@ -0,0 +1 @@
[^0]

View File

@@ -0,0 +1 @@
stderr from bad command line arg '-bad'

View File

@@ -0,0 +1,3 @@
enable_language(CXX)
set(CMAKE_CXX_PVS_STUDIO "${PSEUDO_PVS};-bad")
add_executable(main main.cxx)

View File

@@ -0,0 +1,8 @@
file (GLOB_RECURSE errFiles "${RunCMake_TEST_BINARY_DIR}/*-pvs.err")
file (GLOB_RECURSE logFiles "${RunCMake_TEST_BINARY_DIR}/*-pvs.log")
if (NOT errFiles)
set(RunCMake_TEST_FAILED ".err file not found.")
endif()
if (logFiles)
set(RunCMake_TEST_FAILED "Leftover .log file found:\n ${logFiles}\n.err files:\n ${errFiles}")
endif()

View File

@@ -0,0 +1 @@
example warning

View File

@@ -0,0 +1,3 @@
enable_language(CXX)
set(CMAKE_CXX_PVS_STUDIO "$<1:${PSEUDO_PVS}>")
add_executable(main main.cxx)

View File

@@ -120,12 +120,14 @@ set(properties
"C_CPPLINT" "cpplint" "<SAME>"
"C_CPPCHECK" "cppcheck" "<SAME>"
"C_INCLUDE_WHAT_YOU_USE" "iwyu" "<SAME>"
"C_PVS_STUDIO" "pvs-studio-analyzer" "<SAME>"
## C++
"CXX_CLANG_TIDY" "clang-tidy" "<SAME>"
"CXX_CLANG_TIDY_EXPORT_FIXES_DIR" "${dir}" "<SAME>"
"CXX_CPPLINT" "cpplint" "<SAME>"
"CXX_CPPCHECK" "cppcheck" "<SAME>"
"CXX_INCLUDE_WHAT_YOU_USE" "iwyu" "<SAME>"
"CXX_PVS_STUDIO" "pvs-studio-analyzer" "<SAME>"
## Objective C
"OBJC_CLANG_TIDY" "clang-tidy" "<SAME>"
"OBJC_CLANG_TIDY_EXPORT_FIXES_DIR" "${dir}" "<SAME>"

View File

@@ -0,0 +1,46 @@
#ifndef _CRT_SECURE_NO_WARNINGS
# define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
FILE* fin;
FILE* fout;
char* outFile = NULL;
{
int i;
for (i = 1; i < argc; ++i) {
if (strcmp(argv[i], "-o") == 0) {
outFile = argv[i + 1];
}
}
}
if (outFile == NULL) {
printf("No output file.\n");
return 1;
}
fin = fopen(argv[argc - 1], "r");
if (fin == NULL) {
printf("Error: Could not open input file.\n");
return 1;
}
fout = fopen(outFile, "w");
if (fout == NULL) {
printf("Error: Could not open output file.\n");
fclose(fin);
return 1;
}
{
int ch;
while ((ch = fgetc(fin)) != EOF) {
fputc(ch, fout);
}
}
fprintf(stdout, "Total Messages: 1\n");
fclose(fin);
fclose(fout);
return 0;
}

View File

@@ -0,0 +1,30 @@
#ifndef _CRT_SECURE_NO_WARNINGS
# define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
FILE* f;
int i;
for (i = 1; i < argc; ++i) {
if (strcmp(argv[i], "-bad") == 0) {
fprintf(stdout, "stdout from bad command line arg '-bad'\n");
fprintf(stderr, "stderr from bad command line arg '-bad'\n");
return 1;
}
if (strcmp(argv[i], "--output-file") == 0) {
i++;
f = fopen(argv[i], "w");
if (!f) {
fprintf(stderr, "Error opening %s for writing\n", argv[i]);
return 1;
}
fprintf(f, "discard this line\nexample warning\n");
fclose(f);
}
}
return 0;
}