diff --git a/.github/workflows/coverage-generate.yaml b/.github/workflows/coverage-generate.yaml index 76d8bc98..a261c4ee 100644 --- a/.github/workflows/coverage-generate.yaml +++ b/.github/workflows/coverage-generate.yaml @@ -34,7 +34,7 @@ jobs: node-version: 24 - name: Install dependencies run: | - sudo apt install -y build-essential doxygen graphviz + sudo apt install -y build-essential cppcheck doxygen graphviz npm install -g jsonld-cli npm --prefix scripts ci - name: Install Rust static analysis tools diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 12e5aad5..b5c7f61b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -44,12 +44,12 @@ jobs: if: runner.os == 'Linux' run: | sudo apt update - sudo apt install -y build-essential doxygen graphviz + sudo apt install -y build-essential cppcheck doxygen graphviz - name: Install macOS packages if: runner.os == 'macOS' run: | brew update - brew install doxygen gcc go graphviz + brew install cppcheck doxygen gcc go graphviz - name: Install Rust static analysis tools run: | rustup component add clippy diff --git a/src/shacl2code/lang/templates/cpp/objectset.cpp.j2 b/src/shacl2code/lang/templates/cpp/objectset.cpp.j2 index 1a6de27f..b7278f51 100644 --- a/src/shacl2code/lang/templates/cpp/objectset.cpp.j2 +++ b/src/shacl2code/lang/templates/cpp/objectset.cpp.j2 @@ -45,8 +45,7 @@ void SHACLObjectSet::add(Ref const &ref) { } auto p = ref.obj(); - if (mObjects.find(p) == mObjects.end()) { - mObjects.insert(p); + if (mObjects.insert(p).second) { addIndex(p); } } diff --git a/src/shacl2code/lang/templates/cpp/walk.hpp.j2 b/src/shacl2code/lang/templates/cpp/walk.hpp.j2 index c818f3db..110908e2 100644 --- a/src/shacl2code/lang/templates/cpp/walk.hpp.j2 +++ b/src/shacl2code/lang/templates/cpp/walk.hpp.j2 @@ -31,32 +31,34 @@ template class Ref; */ struct EXPORT WalkCallback { /// Callback when a string property is walked - std::function> const - &)> const &stringProp = [](auto &, auto const &) {}; + std::function> const &)> + stringProp = [](auto &, auto const &) {}; /// Callback when an integer property is walked - std::function)> const &integerProp = - [](auto &, auto const &) {}; + std::function)> integerProp = [](auto &, + auto const &) {}; /// Callback when a boolean property is walked - std::function)> const &booleanProp = [](auto const &, - auto const &) {}; + std::function)> booleanProp = [](auto const &, + auto const &) {}; /// Callback when a double property is walked - std::function)> const &doubleProp = [](auto &, - auto const &) {}; + std::function)> doubleProp = [](auto &, auto const &) {}; /// Callback when a DateTime property is walked - std::function> const - &)> const &dateTimeProp = [](auto &, auto const &) {}; + std::function> const &)> + dateTimeProp = [](auto &, auto const &) {}; /// Callback when an Enum property is walked - std::function> const - &)> const &enumProp = [](auto &, auto const &) {}; + std::function> const &)> + enumProp = [](auto &, auto const &) {}; /// Callback when an Any URI property is walked - std::function> const - &)> const &anyURIProp = [](auto &, auto const &) {}; + std::function> const &)> + anyURIProp = [](auto &, auto const &) {}; /** * @brief Callback when an object is walked @@ -65,8 +67,8 @@ struct EXPORT WalkCallback { * (that is, all its properties walked), or false if not */ std::function const>> const &)> const - &refProp = [](auto &, auto const &) { return false; }; + std::optional const>> const &)> + refProp = [](auto &, auto const &) { return false; }; }; /* {{ api_def_end }} */ diff --git a/tests/test_cpp.py b/tests/test_cpp.py index 94a312db..958a7c8a 100644 --- a/tests/test_cpp.py +++ b/tests/test_cpp.py @@ -1,6 +1,10 @@ # # Copyright (c) 2024 Joshua Watt # +# SPDX-FileContributor: Joshua Watt +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2024 Joshua Watt +# SPDX-FileType: SOURCE # SPDX-License-Identifier: MIT import json @@ -275,7 +279,7 @@ def f(code_fragment, *, progress=Progress.RUNS, static=False): yield f -@pytest.mark.parametrize( +CPP_MODEL_TESTS = ( "args,basename", [ ( @@ -288,25 +292,32 @@ def f(code_fragment, *, progress=Progress.RUNS, static=False): ), ], ) + + +def _generate_cpp(tmp_path, args, basename): + subprocess.run( + [ + "shacl2code", + "generate", + ] + + args + + [ + "cpp", + "--output", + tmp_path / basename, + "--version=0.0.1", + ], + check=True, + ) + + +@pytest.mark.parametrize(*CPP_MODEL_TESTS) class TestOutput: def test_trailing_whitespace(self, tmp_path, args, basename): """ Tests that the generated file does not have trailing whitespace """ - subprocess.run( - [ - "shacl2code", - "generate", - ] - + args - + [ - "cpp", - "--output", - tmp_path / basename, - "--version=0.0.1", - ], - check=True, - ) + _generate_cpp(tmp_path, args, basename) for fn in tmp_path.iterdir(): with fn.open("r") as f: @@ -320,20 +331,7 @@ def test_tabs(self, tmp_path, args, basename): """ Tests that the output file doesn't contain tabs """ - subprocess.run( - [ - "shacl2code", - "generate", - ] - + args - + [ - "cpp", - "--output", - tmp_path / basename, - "--version=0.0.1", - ], - check=True, - ) + _generate_cpp(tmp_path, args, basename) for fn in tmp_path.iterdir(): if fn.name == "Makefile": @@ -346,20 +344,7 @@ def test_tabs(self, tmp_path, args, basename): ), f"{fn}: Line {lineno + 1} has tabs: {line!r}" def test_output_compile(self, tmp_path, args, basename): - subprocess.run( - [ - "shacl2code", - "generate", - ] - + args - + [ - "cpp", - "--output", - tmp_path / basename, - "--version=0.0.1", - ], - check=True, - ) + _generate_cpp(tmp_path, args, basename) make_args = [ "make", @@ -383,6 +368,42 @@ def test_output_compile(self, tmp_path, args, basename): ) +@pytest.mark.parametrize(*CPP_MODEL_TESTS) +class TestStaticAnalysis: + """ + Static analysis checks for the generated C++ code + """ + + def test_cppcheck(self, tmp_path, args, basename): + """ + cppcheck static analysis. + + Excludes the bundled "-jsonld" source, not generated code. + + duplInheritedMember is suppressed: every generated class + deliberately redeclares its own static Type descriptor. + """ + _generate_cpp(tmp_path, args, basename) + + sources = sorted( + p + for p in tmp_path.iterdir() + if p.suffix in (".c", ".cpp", ".h", ".hpp") and "-jsonld" not in p.name + ) + + subprocess.run( + [ + "cppcheck", + "--enable=performance,portability,warning", + "--error-exitcode=1", + "--suppress=duplInheritedMember", + ] + + sources, + check=True, + cwd=tmp_path, + ) + + def test_compile(compile_test): compile_test("")