Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
description: Find warnings/errors in C, C++, and Objective-C code
types_or: [c, c++, c#, objective-c]
language: python
require_serial: true
- id: cpplint
name: cpplint
entry: cpplint-hook
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
This is a [pre-commit](https://pre-commit.com) hooks repo that
integrates two C/C++ code formatters:
> [clang-format](https://clang.llvm.org/docs/ClangFormatStyleOptions.html),
[uncrustify](http://uncrustify.sourceforge.net/),
[uncrustify](https://uncrustify.sourceforge.net/),

and five C/C++ static code analyzers:
> [clang-tidy](https://clang.llvm.org/extra/clang-tidy/),
[oclint](http://oclint.org/),
[cppcheck](http://cppcheck.sourceforge.net/),
[oclint](https://oclint.org/),
[cppcheck](https://cppcheck.sourceforge.net/),
[cpplint](https://github.com/cpplint/cpplint),
[include-what-you-use](https://github.com/include-what-you-use/include-what-you-use)

Expand Down Expand Up @@ -238,7 +238,7 @@ These options are automatically added to enable all errors or are required.

* oclint: `["-enable-global-analysis", "-enable-clang-static-analyzer", "-max-priority-3", "0"]`
* uncrustify: `["-c", "defaults.cfg", "-q"]` (options added, and a defaults.cfg generated, if -c is missing)
* cppcheck: `["-q" , "--error-exitcode=1", "--enable=all", "--suppress=unmatchedSuppression", "--suppress=missingIncludeSystem", "--suppress=unusedFunction"]` (See https://github.com/pocc/pre-commit-hooks/pull/30)
* cppcheck: `["-q" , "--error-exitcode=1", "--enable=all", "--suppress=unmatchedSuppression", "--suppress=missingIncludeSystem"]` (See https://github.com/pocc/pre-commit-hooks/pull/30)
* cpplint: `["--verbose=0"]`

If you supply any of these options in `args:`, your options will override the above defaults (use `-<flag>=<option>` if possible when overriding).
Expand Down
103 changes: 96 additions & 7 deletions hooks/cppcheck.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#!/usr/bin/env python3
"""Wrapper script for cppcheck."""
import os
import shutil
import sys
import tempfile
from typing import List

from hooks.utils import StaticAnalyzerCmd
Expand All @@ -15,27 +18,113 @@ class CppcheckCmd(StaticAnalyzerCmd):
def __init__(self, args: List[str]):
super().__init__(self.command, self.lookbehind, args)
self.parse_args(args)
self.cppcheck_build_dir = None

# quiet for stdout purposes
self.add_if_missing(["-q"])
# make cppcheck behave as expected for pre-commit
self.add_if_missing(["--error-exitcode=1"])
# Enable all of the checks
self.add_if_missing(["--enable=all"])
# Per https://github.com/pocc/pre-commit-hooks/pull/30, suppress missingIncludeSystem messages
self.add_if_missing(
["--suppress=unmatchedSuppression", "--suppress=missingIncludeSystem", "--suppress=unusedFunction"]
)
# Per https://github.com/pocc/pre-commit-hooks/pull/30, suppress messages
self.add_if_missing([
"--suppress=unmatchedSuppression",
"--suppress=missingIncludeSystem",
# "--suppress=unusedFunction" # Because the cppcheck-build-dir is provided, this is not needed
])
# Add -j option with number of available processors unless already provided
self.add_j_option()

def cleanup_build_dir(self):
"""Remove the temporary build directory if it exists."""
if self.cppcheck_build_dir and os.path.exists(self.cppcheck_build_dir):
try:
shutil.rmtree(self.cppcheck_build_dir)
except OSError:
pass
self.cppcheck_build_dir = None

def get_j_value(self):
"""Get the value of -j option if present, None otherwise."""
for i, arg in enumerate(self.args):
if arg == "-j":
# -j followed by value
if i + 1 < len(self.args) and not self.args[i + 1].startswith("-"):
try:
return int(self.args[i + 1])
except ValueError:
return None
elif arg.startswith("-j"):
# -jN format
try:
return int(arg[2:])
except ValueError:
return None
return None

def add_j_option(self):
"""Add -j option with number of available processors unless already provided."""
# Check if -j is already in args
j_value = self.get_j_value()

if j_value is not None:
# -j is already specified by user
if j_value > 1:
# Check if --cppcheck-build-dir is present
has_build_dir = any(arg.startswith("--cppcheck-build-dir") for arg in self.args)
if not has_build_dir:
# Create a unique temporary directory for parallel processing
self.cppcheck_build_dir = tempfile.mkdtemp(prefix="cppcheck_")
self.add_if_missing([f"--cppcheck-build-dir={self.cppcheck_build_dir}"])
return

# Get number of available processors
try:
import multiprocessing
num_cpus = multiprocessing.cpu_count()
except (ImportError, NotImplementedError):
# Fallback if multiprocessing is not available
num_cpus = 1

# Only add -j if > 1, otherwise single-threaded is fine
if num_cpus > 1:
self.cppcheck_build_dir = tempfile.mkdtemp(prefix="cppcheck_")
self.args.extend(["-j", str(num_cpus), f"--cppcheck-build-dir={self.cppcheck_build_dir}"])

def run(self):
"""Run cppcheck"""
for filename in self.files:
self.run_command([filename] + self.args)
if not self.files:
return

# Create a temporary file with the list of all files
file_list_path = None
try:
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
file_list_path = f.name
for filename in self.files:
f.write(filename + "\n")

# Run cppcheck with the file list using --file-list=FILELIST format
self.run_command(["--file-list=" + file_list_path] + self.args)
self.exit_on_error()
finally:
# Clean up the temporary files
if file_list_path and os.path.exists(file_list_path):
try:
os.unlink(file_list_path)
except OSError:
pass
# Clean up the build directory
self.cleanup_build_dir()


def main(argv: List[str] = sys.argv):
cmd = CppcheckCmd(argv)
cmd.run()
try:
cmd.run()
finally:
# Ensure cleanup even if run() raises an exception
cmd.cleanup_build_dir()


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion tests/table_tests_integration.json
Original file line number Diff line number Diff line change
Expand Up @@ -610,4 +610,4 @@
"expd_output": "oclint...................................................................Failed\n- hook id: oclint\n- exit code: 6\n\nCompiler Errors:\n(please be aware that these errors will prevent OCLint from analyzing this source code)\n\n{test_dir}/err.cpp:2:18: non-void function 'main' should return a value\n\nClang Static Analyzer Results:\n\n{test_dir}/err.cpp:2:18: non-void function 'main' should return a value\n\n\nOCLint Report\n\nSummary: TotalFiles=0 FilesWithViolations=0 P1=0 P2=0 P3=0 \n\n\n[OCLint (http://oclint.org) v{oclint_ver}]\n\n",
"expd_retcode": 1
}
]
]
113 changes: 113 additions & 0 deletions tests/test_cppcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Unit tests for cppcheck hook."""
import os
import sys
import tempfile

import pytest

from hooks.cppcheck import CppcheckCmd


class TestCppcheckAddJOption:
"""Test the add_j_option method."""

def setup_method(self):
"""Create temporary test files."""
self.test_dir = tempfile.mkdtemp()
self.test_file = os.path.join(self.test_dir, "test.c")
with open(self.test_file, "w") as f:
f.write("int main() { return 0; }\n")

def teardown_method(self):
"""Clean up temporary files."""
import shutil

shutil.rmtree(self.test_dir, ignore_errors=True)

def test_add_j_option_when_not_provided(self):
"""Test that -j option is added when not provided."""
sys.argv = ["cppcheck-hook", self.test_file]
cmd = CppcheckCmd(sys.argv)
# Check that -j is in args
j_args = [arg for arg in cmd.args if arg.startswith("-j")]
assert len(j_args) == 1, f"Expected 1 -j option, found {len(j_args)}: {j_args}"
# Check that the value is a number
if j_args[0] == "-j":
# -j is separate from the value
j_index = cmd.args.index("-j")
assert j_index + 1 < len(cmd.args), "Expected value after -j"
assert cmd.args[j_index + 1].isdigit(), f"Expected digit after -j, found {cmd.args[j_index + 1]}"
else:
# -jN format
assert j_args[0][2:].isdigit(), f"Expected digit in {j_args[0]}"

def test_add_j_option_when_provided_with_space(self):
"""Test that -j option is not duplicated when provided as -j 2."""
sys.argv = ["cppcheck-hook", "-j", "2", self.test_file]
cmd = CppcheckCmd(sys.argv)
# Check that only one -j option exists
j_args = [arg for arg in cmd.args if arg.startswith("-j")]
assert len(j_args) == 1, f"Expected 1 -j option, found {len(j_args)}: {j_args}"
assert "-j" in cmd.args, "Expected -j in args"
assert "2" in cmd.args, "Expected 2 in args"

def test_add_j_option_when_provided_without_space(self):
"""Test that -j option is not duplicated when provided as -j2."""
sys.argv = ["cppcheck-hook", "-j2", self.test_file]
cmd = CppcheckCmd(sys.argv)
# Check that only one -j option exists
j_args = [arg for arg in cmd.args if arg.startswith("-j")]
assert len(j_args) == 1, f"Expected 1 -j option, found {len(j_args)}: {j_args}"
assert "-j2" in cmd.args, "Expected -j2 in args"

def test_add_j_option_with_multiple_files(self):
"""Test that -j option is added correctly with multiple files."""
test_file2 = os.path.join(self.test_dir, "test2.c")
with open(test_file2, "w") as f:
f.write("int foo() { return 1; }\n")

sys.argv = ["cppcheck-hook", self.test_file, test_file2]
cmd = CppcheckCmd(sys.argv)
# Check that -j is in args
j_args = [arg for arg in cmd.args if arg.startswith("-j")]
assert len(j_args) == 1, f"Expected 1 -j option, found {len(j_args)}: {j_args}"
# Check that both files are in cmd.files
assert len(cmd.files) == 2, f"Expected 2 files, found {len(cmd.files)}"


class TestCppcheckFileList:
"""Test the file list functionality."""

def setup_method(self):
"""Create temporary test files."""
self.test_dir = tempfile.mkdtemp()
self.test_file1 = os.path.join(self.test_dir, "test1.c")
self.test_file2 = os.path.join(self.test_dir, "test2.c")
with open(self.test_file1, "w") as f:
f.write("int main() { return 0; }\n")
with open(self.test_file2, "w") as f:
f.write("int foo() { return 1; }\n")

def teardown_method(self):
"""Clean up temporary files."""
import shutil

shutil.rmtree(self.test_dir, ignore_errors=True)

def test_file_list_created(self):
"""Test that a file list is created with all files."""
sys.argv = ["cppcheck-hook", self.test_file1, self.test_file2]
cmd = CppcheckCmd(sys.argv)
# The files should be in cmd.files
assert len(cmd.files) == 2, f"Expected 2 files, found {len(cmd.files)}"
assert self.test_file1 in cmd.files, f"Expected {self.test_file1} in files"
assert self.test_file2 in cmd.files, f"Expected {self.test_file2} in files"

def test_single_file(self):
"""Test that single file works correctly."""
sys.argv = ["cppcheck-hook", self.test_file1]
cmd = CppcheckCmd(sys.argv)
# The file should be in cmd.files
assert len(cmd.files) == 1, f"Expected 1 file, found {len(cmd.files)}"
assert self.test_file1 in cmd.files, f"Expected {self.test_file1} in files"
4 changes: 3 additions & 1 deletion tests/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
from hooks.cpplint import CpplintCmd
from hooks.oclint import OCLintCmd
from hooks.uncrustify import UncrustifyCmd
from hooks.utils import Command, FormatterCmd, StaticAnalyzerCmd
from hooks.utils import Command
from hooks.utils import FormatterCmd
from hooks.utils import StaticAnalyzerCmd


class TestArgumentParsing:
Expand Down
7 changes: 6 additions & 1 deletion tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ def generate_clang_tidy_tests(cls):

@classmethod
def generate_cppcheck_tests(cls):
cppcheck_arg_sets = [[]]
# Test with no additional args, with -j option, and with -j2 option
cppcheck_arg_sets = [[], ["-j", "2"], ["-j2"]]
# cppcheck adds unnecessary error information.
# See https://stackoverflow.com/questions/6986033
if cls.versions["cppcheck"] <= "1.88":
Expand Down Expand Up @@ -570,6 +571,10 @@ def run_shell_cmd(cmd_name, files, args, _, target_output, target_retcode):
actual = filtered_actual
# Newer cppcheck versions include a checkers report info line
if cmd_name == "cppcheck":
# Filter out warning about -j option and unusedFunction check
actual = re.sub(rb"cppcheck: unusedFunction check can't be used with '-j' option\. Disabling unusedFunction check\.\n", b"", actual)
# Filter out warning about -j option requiring --cppcheck-build-dir
actual = re.sub(rb"cppcheck: unusedFunction check requires --cppcheck-build-dir to be active with -j\.\n", b"", actual)
actual = re.sub(rb"\nnofile:0:0: information: Active checkers.*\n+", b"\n", actual)
# Windows cppcheck adds extra trailing newlines - normalize both to have exactly one
if actual and actual.strip(): # Don't normalize empty output
Expand Down
2 changes: 1 addition & 1 deletion tests/test_logic_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def test_cppcheck_default_args_added(self):
assert "--enable=all" in cmd.args
assert "--suppress=unmatchedSuppression" in cmd.args
assert "--suppress=missingIncludeSystem" in cmd.args
assert "--suppress=unusedFunction" in cmd.args
# assert "--suppress=unusedFunction" in cmd.args # Not needed
finally:
if os.path.exists(temp_file):
os.unlink(temp_file)
Expand Down
7 changes: 5 additions & 2 deletions tests/test_utils_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
import os
import subprocess as sp
import tempfile
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock
from unittest.mock import patch

import pytest

from hooks.utils import Command, FormatterCmd, StaticAnalyzerCmd
from hooks.utils import Command
from hooks.utils import FormatterCmd
from hooks.utils import StaticAnalyzerCmd


class TestCommandBaseClass:
Expand Down