PVS-Studio — Deep Static Analysis for C++
PVS-Studio reference: running analysis, suppressing false positives, CI integration, and understanding diagnostic categories V501–V2xxx.
TL;DR
PVS-Studio is a commercial static analyzer with a free tier for open-source projects. It performs deep interprocedural analysis to catch real bugs that survive code review and unit tests: copy-paste errors, null dereferences, integer overflow, use-after-free, and more.
- Deeper analysis than clang-tidy or cppcheck — follows data flow across function boundaries
- MISRA C/C++ and OWASP compliance checking built in
- Free license available for open-source, academic, and community projects
- SARIF output for native GitHub Code Scanning integration
Licensing and Free Tier
| License type | Conditions |
|---|---|
| Commercial | Paid per developer seat; supports all features |
| Open-source | Free; request at pvs-studio.com/open-source-license |
| Academic | Free for universities and non-commercial research |
| Trial | Analyzes everything but limits output to first 10 warnings per file |
To activate an open-source or trial license:
pvs-studio-analyzer credentials YOUR_NAME YOUR_KEY -o pvs-studio.licThe license file is picked up automatically from the current directory or ~/.config/PVS-Studio/.
Installation
Linux (.deb / .rpm)
# Debian/Ubuntu
wget -q https://files.pvs-studio.com/pvs-studio-latest.deb
sudo dpkg -i pvs-studio-latest.deb
# RHEL/Fedora
wget -q https://files.pvs-studio.com/pvs-studio-latest.rpm
sudo rpm -i pvs-studio-latest.rpmAfter installation the key tools are on PATH:
pvs-studio-analyzer— runs analysisplog-converter— converts binary.logoutput to text, HTML, XML, SARIF, etc.
Windows
Download the Visual Studio plugin or standalone installer from pvs-studio.com/downloads. The plugin adds an integrated PVS-Studio menu to Visual Studio 2019/2022.
For command-line use on Windows, PVS-Studio_Cmd.exe accepts a .sln or .vcxproj file:
PVS-Studio_Cmd.exe ^
--target MyProject.sln ^
--configuration Release ^
--output pvs.logmacOS
# Download the .pkg from pvs-studio.com/downloads and install, then:
brew install bear # needed for Makefile-based projectsDocker
docker pull pvsstudio/pvs-studio:latest
# Mount your source and run inside the container
docker run --rm \
-v $(pwd):/workspace \
-w /workspace \
pvsstudio/pvs-studio:latest \
pvs-studio-analyzer analyze \
-f build/compile_commands.json \
-o pvs.log \
-j$(nproc)Basic Analysis with CMake
CMake's compile_commands.json is the preferred input — it provides exact compiler flags per translation unit.
# Step 1: generate compile_commands.json
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build
# Step 2: run analysis
pvs-studio-analyzer analyze \
-f build/compile_commands.json \
-o pvs-report.log \
-j$(nproc)
# Step 3: convert to human-readable text
plog-converter \
-a GA:1,2,3 \
-t errorfile \
-o pvs-report.txt \
pvs-report.log
# Or produce an HTML report (self-contained, good for local review)
plog-converter \
-a GA:1,2,3 \
-t html \
-o pvs-report.html \
pvs-report.logThe -a GA:1,2,3 flag selects General Analysis at all three confidence levels. To limit to high-confidence warnings only, use -a GA:1.
Analysis with Makefile / Bear
When a project uses plain make (no CMake), use bear to intercept the build and generate compile_commands.json:
# Install bear
sudo apt install bear # Debian/Ubuntu
brew install bear # macOS
# Intercept the build
bear -- make -j$(nproc)
# compile_commands.json is now in the current directory
pvs-studio-analyzer analyze \
-f compile_commands.json \
-o out.log \
-j$(nproc)
plog-converter -a GA:1,2 -t errorfile -o out.txt out.logAnalysis with compile_commands.json (direct)
If your build system already produces compile_commands.json (Meson, Bazel with --experimental_generate_json_compilation_database, etc.) just point PVS-Studio at it:
pvs-studio-analyzer analyze \
-f path/to/compile_commands.json \
-o pvs.logUnderstanding Diagnostic Categories
PVS-Studio diagnostics follow the pattern VXXX where the leading digit(s) identify the ruleset:
| Range | Ruleset | What it catches |
|---|---|---|
| V1xx | General Analysis (GA) | Logic errors, dead code, questionable constructs |
| V2xx | General Analysis (GA) | Null dereferences, uninitialized memory, OOB |
| V3xx | 64-bit migration | Pointer truncation, sign-extension issues |
| V5xx | MISRA C/C++ | MISRA rule violations |
| V6xx | OWASP | Security-relevant patterns (injection, format string) |
| V7xx | General Analysis (GA) | Copy-paste detection, identical branches |
| V10xx | General Analysis (GA) | Lifetime, use-after-free, dangling references |
| V20xx | Customers' requests | Diagnostics added at user request |
Each diagnostic also carries a level (1–3) reflecting confidence:
- Level 1 — high confidence; almost always a real bug
- Level 2 — medium confidence; likely a bug but context-dependent
- Level 3 — low confidence; informational, review manually
Common Findings with Examples
V501 — Identical sub-expressions
// Typo: both sides of == compare x to itself
if (a.x == a.x) // should be a.x == b.x
return true;V512 — memset on a non-POD type
std::string name;
memset(&name, 0, sizeof(name)); // UB: stomps internal state of std::stringV522 — Potential null pointer dereference after dynamic_cast
Base* base = getBase();
auto* derived = dynamic_cast<Derived*>(base);
derived->process(); // derived is null if cast failsFix:
if (auto* derived = dynamic_cast<Derived*>(base))
derived->process();V547 — Expression is always true/false
unsigned int x = getValue();
if (x >= 0) // always true for unsigned
doSomething();V601 — Implicit bool-to-int conversion in return
int enabled() {
return flag == STATE_ACTIVE; // returns 0 or 1, may be unintentional
}V611 — Memory allocated with new[], freed with delete
int* arr = new int[64];
delete arr; // UB: should be delete[]V776 — Potentially infinite loop (flag not volatile)
bool stop = false;
void worker() {
while (!stop); // optimizer may cache stop in a register
}Fix: use std::atomic<bool> or volatile bool.
V1004 — Pointer used outside the null-check scope
if (ptr != nullptr) {
log("ptr is valid");
}
ptr->doWork(); // ptr used here, outside the guard — may be nullV2010 — Suspicious integer shift
int64_t mask = 1 << 40; // 1 is int (32-bit); shift > 31 is UB
// Fix: 1LL << 40Suppressing False Positives
Inline suppression (single line)
int result = legacyFunc(); //-V610Block suppression
//-V::512,611
char buf[64];
memset(buf, 0, sizeof(buf));
//+V::512,611Suppress file (pvs-studio.suppress)
Generated by pvs-studio-analyzer suppress; stores per-warning fingerprints so suppressed findings are not shown even when line numbers shift:
// DO NOT EDIT — auto-generated suppress file
V501:/home/user/project/src/algo.cpp:42
V522:/home/user/project/src/render.cpp:188Apply at conversion time:
plog-converter \
-a GA:1,2 \
-t errorfile \
--suppress-file pvs-studio.suppress \
-o filtered.txt \
pvs.logCMake-level exclusion of third-party directories
Exclude paths entirely so PVS-Studio never analyzes vendored code:
pvs-studio-analyzer analyze \
-f build/compile_commands.json \
-o pvs.log \
--exclude-path third_party/ \
--exclude-path build/_deps/ \
-j$(nproc)Baseline / Suppress File Workflow
On large existing codebases, create a baseline on the first run to silence all pre-existing warnings. From that point on, only new warnings introduced by fresh commits are reported — useful for incremental adoption.
# 1. First-ever run: capture all existing warnings
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build
pvs-studio-analyzer analyze -f build/compile_commands.json -o baseline.log -j$(nproc)
# 2. Create suppress file from all current findings
pvs-studio-analyzer suppress --project=. baseline.log
# Writes pvs-studio.suppress
# 3. Commit pvs-studio.suppress to version control
git add pvs-studio.suppress
git commit -m "chore: add PVS-Studio baseline suppress file"
# 4. Subsequent CI runs: only new warnings appear
pvs-studio-analyzer analyze -f build/compile_commands.json -o new.log -j$(nproc)
pvs-studio-analyzer filter-suppressed pvs-studio.suppress new.log ci.log
plog-converter -a GA:1,2 -t errorfile -o ci-report.txt ci.logAs you fix pre-existing warnings, remove them from the suppress file to ratchet quality up over time.
CI Integration — GitHub Actions
Basic job
name: Static Analysis
on: [push, pull_request]
jobs:
pvs-studio:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install PVS-Studio
run: |
wget -q https://files.pvs-studio.com/pvs-studio-latest.deb
sudo dpkg -i pvs-studio-latest.deb
- name: Configure and build
run: |
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build -j$(nproc)
- name: Activate license
run: |
pvs-studio-analyzer credentials \
"${{ secrets.PVS_NAME }}" \
"${{ secrets.PVS_KEY }}" \
-o pvs-studio.lic
- name: Run PVS-Studio
run: |
pvs-studio-analyzer analyze \
-f build/compile_commands.json \
-o pvs.log \
-j$(nproc)
- name: Convert to text report
run: |
plog-converter -a GA:1,2 -t errorfile -o pvs-report.txt pvs.log
cat pvs-report.txt
- name: Upload report artifact
uses: actions/upload-artifact@v4
with:
name: pvs-studio-report
path: pvs-report.txtWith GitHub Code Scanning (SARIF)
SARIF output lets GitHub display PVS-Studio findings inline on pull requests under the Security tab:
- name: Convert to SARIF
run: |
plog-converter \
-a GA:1,2 \
-t sarif \
-o pvs.sarif \
pvs.log
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: pvs.sarif
if: always() # upload even when analysis finds errorsIncremental check (new warnings only)
- name: Filter suppressed baseline
run: |
pvs-studio-analyzer filter-suppressed \
pvs-studio.suppress pvs.log filtered.log
plog-converter -a GA:1,2 -t errorfile -o filtered.txt filtered.log
# Fail CI if any new warnings remain
grep -q "No messages generated" filtered.txt || \
(cat filtered.txt && exit 1)CMake Integration (Analysis as Build Step)
You can wire PVS-Studio into CMake so that cmake --build triggers analysis automatically:
# CMakeLists.txt
find_program(PVS_STUDIO_ANALYZER pvs-studio-analyzer)
find_program(PLOG_CONVERTER plog-converter)
if(PVS_STUDIO_ANALYZER AND PLOG_CONVERTER)
option(ENABLE_PVS_STUDIO "Run PVS-Studio on every build" OFF)
if(ENABLE_PVS_STUDIO)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_custom_target(pvs_analysis ALL
COMMAND ${PVS_STUDIO_ANALYZER} analyze
-f ${CMAKE_BINARY_DIR}/compile_commands.json
-o ${CMAKE_BINARY_DIR}/pvs.log
-j ${CMAKE_BUILD_PARALLEL_LEVEL}
COMMAND ${PLOG_CONVERTER}
-a GA:1,2 -t errorfile
-o ${CMAKE_BINARY_DIR}/pvs-report.txt
${CMAKE_BINARY_DIR}/pvs.log
DEPENDS ${PROJECT_NAME}
COMMENT "Running PVS-Studio static analysis..."
)
endif()
endif()Enable with:
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DENABLE_PVS_STUDIO=ON
cmake --build buildOutput Format Reference
plog-converter supports several -t targets:
| Target | Description |
|---|---|
errorfile | GCC-style file:line: warning: ... (works in most editors) |
html | Self-contained HTML report with filtering UI |
xml | XML (can be consumed by Jenkins Warnings NG plugin) |
sarif | SARIF 2.1.0 (GitHub Code Scanning, Azure DevOps) |
csv | Comma-separated; good for spreadsheet review |
tasks | Qt Creator task list format |
teamcity | TeamCity service messages |
Filter by analysis group and level with -a:
# GA level 1 and 2 only (recommended for CI gate)
plog-converter -a GA:1,2 -t errorfile -o report.txt pvs.log
# GA + MISRA level 1 only
plog-converter -a GA:1,MISRA:1 -t errorfile -o report.txt pvs.log
# Everything
plog-converter -a GA:1,2,3,64:1,2,3,OP:1,2,3,CS:1,2,3,MISRA:1,2,3 \
-t html -o report.html pvs.logComparison: PVS-Studio vs clang-tidy vs cppcheck
| Feature | PVS-Studio | clang-tidy | cppcheck |
|---|---|---|---|
| Analysis depth | Deep (interprocedural, data-flow) | Moderate (AST + CFG) | Moderate (token + CFG) |
| False positive rate | Low | Medium | Low–Medium |
| Speed | Moderate | Fast | Fast |
| Cost | Commercial (free OSS tier) | Free | Free |
| MISRA C/C++ support | Yes | No | Partial (addon) |
| OWASP support | Yes | No | No |
| SARIF output | Yes | Via wrapper | Via converter |
| IDE integration | VS, CLion, Rider, QtCreator | VS Code, CLion, Emacs | CLion, VS Code |
| Interprocedural analysis | Yes | Limited | Limited |
| CI integration | Yes | Yes | Yes |
| Copy-paste detection | Yes (V7xx) | No | No |
Choose PVS-Studio when:
- You need MISRA or OWASP compliance reports
- The codebase is large and mature (interprocedural analysis pays off)
- False positive noise from clang-tidy or cppcheck is too high
Combine all three for best coverage: clang-tidy catches modernization opportunities during development, cppcheck is fast and free, PVS-Studio runs in CI for deep coverage.
Practical Tips
Start with level 1 only. Run plog-converter -a GA:1 in CI first. Fix everything it finds before enabling level 2. Level 3 is mostly informational.
Exclude generated code. Protobuf, Qt MOC, and similar generated headers produce many false positives. Use --exclude-path or add them to the suppress file during baseline creation.
Use --incremental mode for fast PR checks. PVS-Studio can cache analysis results per translation unit and only re-analyze files that changed:
pvs-studio-analyzer analyze \
-f build/compile_commands.json \
-o pvs.log \
--incremental \
--incremental-db pvs-cache/ \
-j$(nproc)Investigate V1xx before V3xx. V3xx (64-bit issues) matters for porting; if you are targeting only 64-bit platforms already, defer those until you need them.
Treat suppresses as debt. Track suppress file size in CI (wc -l pvs-studio.suppress) and set a ratchet: the suppress file must not grow larger on main.
See Also
- clang-tidy — fast AST-based checks, style enforcement, modernize transforms
- cppcheck — free, no compilation needed, good for legacy code
- sanitizers — runtime detection of UB, memory errors, data races
- static-analysis — overview comparing multiple tools
- address-sanitizer — memory error detection at runtime