From 26a836d0a79ba6b2cd6b1c54c7cccf71420fc8ff Mon Sep 17 00:00:00 2001 From: Jakob Gamper <97gamjak@gmail.com> Date: Sun, 27 Sep 2026 10:53:44 +0200 Subject: [PATCH 1/4] Disallow EXPECT_THROW and ASSERT_THROW Fixes #126 --- CHANGELOG.md | 6 + docs/source/sections/configuration.rst | 46 +++++++ src/devops/config/config_cpp.py | 3 + .../cpp/ast/checks/macro_replacement.py | 123 ++++++++++++++++++ src/devops/cpp/ast/registry.py | 2 + tests/cpp/ast/test_macro_replacement.py | 120 +++++++++++++++++ 6 files changed, 300 insertions(+) create mode 100644 src/devops/cpp/ast/checks/macro_replacement.py create mode 100644 tests/cpp/ast/test_macro_replacement.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3454099..becce6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## Next Release +### Features + +#### CPP Rules + +- Add `macroReplacement` AST check flagging invocations of a banned macro and suggesting its replacement, e.g. disallowing gtest's `EXPECT_THROW`/`ASSERT_THROW` in favor of custom `EXPECT_THROW_MSG`/`ASSERT_THROW_MSG` macros that also require a failure message. Ships with that mapping as a built-in default (no configuration required) and is fully configurable/extensible via `[cpp.ast_check_config.macroReplacement].macro_to_replacement`. Detection matches the macro name exactly and works whether the macro is defined in the same file or an included header + ## [0.4.2](https://github.com/repo/owner/releases/tag/0.4.2) - 2026-09-26 diff --git a/docs/source/sections/configuration.rst b/docs/source/sections/configuration.rst index 0e3d8a9..d6a1fde 100644 --- a/docs/source/sections/configuration.rst +++ b/docs/source/sections/configuration.rst @@ -376,6 +376,52 @@ with:: [cpp] ast_check_disabled_ids = ["noThrowParen"] +.. rubric:: ``macroReplacement`` + +Flags invocations of a banned macro and suggests the replacement macro that +should be used instead. Ships with a built-in default mapping and requires no +configuration to use: + +.. list-table:: + :header-rows: 1 + :widths: 30 30 + + * - Banned macro + - Use instead + * - ``EXPECT_THROW`` + - ``EXPECT_THROW_MSG`` + * - ``ASSERT_THROW`` + - ``ASSERT_THROW_MSG`` + +Detection matches the macro name exactly (``EXPECT_THROW`` never matches +``EXPECT_THROW_MSG``) and works regardless of whether the macro is defined in +the same file or an included header (e.g. a gtest header). + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Key + - Type + - Default + - Description + * - ``macro_to_replacement`` + - table of string → string + - the built-in mapping above + - Replaces the built-in mapping entirely when set — extend it by + re-listing the defaults you want to keep alongside your own. + +.. code-block:: toml + + [cpp.ast_check_config.macroReplacement] + macro_to_replacement = { EXPECT_THROW = "EXPECT_THROW_MSG", + ASSERT_THROW = "ASSERT_THROW_MSG" } + +To disable this check for a project set:: + + [cpp] + ast_check_disabled_ids = ["macroReplacement"] + ``[file]`` ^^^^^^^^^^ diff --git a/src/devops/config/config_cpp.py b/src/devops/config/config_cpp.py index f0cc2c9..0216287 100644 --- a/src/devops/config/config_cpp.py +++ b/src/devops/config/config_cpp.py @@ -148,6 +148,9 @@ def to_toml_lines(self) -> list[str]: '#disabled_names = ["std::literals"]\n' "#[cpp.ast_check_config.noGlobalUsingEnum]\n" '#disabled_names = ["molsys::LegacyZone"]\n' + "#[cpp.ast_check_config.macroReplacement]\n" + '#macro_to_replacement = { EXPECT_THROW = "EXPECT_THROW_MSG", ' + 'ASSERT_THROW = "ASSERT_THROW_MSG" }\n' ) isf = ( diff --git a/src/devops/cpp/ast/checks/macro_replacement.py b/src/devops/cpp/ast/checks/macro_replacement.py new file mode 100644 index 0000000..58d5ee2 --- /dev/null +++ b/src/devops/cpp/ast/checks/macro_replacement.py @@ -0,0 +1,123 @@ +"""Flag banned macro invocations and suggest their replacement. + +Any macro invocation whose name matches a configured key in +``macro_to_replacement`` is reported, naming the replacement macro that +should be used instead. Built for cases like disallowing gtest's +``EXPECT_THROW``/``ASSERT_THROW`` in favor of custom +``EXPECT_THROW_MSG``/``ASSERT_THROW_MSG`` macros that also require a +failure message, but works for any macro pair. + +Detection is based on libclang's ``MACRO_INSTANTIATION`` cursor (available +because the AST engine always parses with +``PARSE_DETAILED_PROCESSING_RECORD``), so it works regardless of whether the +macro is defined in the same file or an included header, and it matches the +macro name exactly — ``EXPECT_THROW`` never accidentally matches +``EXPECT_THROW_MSG``. + +Ships with a built-in default mapping and requires no configuration to use:: + + EXPECT_THROW -> EXPECT_THROW_MSG + ASSERT_THROW -> ASSERT_THROW_MSG + +Replace or extend it via:: + + [cpp.ast_check_config.macroReplacement] + macro_to_replacement = { EXPECT_THROW = "EXPECT_THROW_MSG", + ASSERT_THROW = "ASSERT_THROW_MSG" } + +To disable this check for a project set:: + + [cpp] + ast_check_disabled_ids = ["macroReplacement"] +""" + +from __future__ import annotations + +import clang.cindex as clang + +from devops.config.base import ConfigError +from devops.cpp.ast.base import Check, Diagnostic + +_DEFAULT_MACRO_TO_REPLACEMENT = { + "EXPECT_THROW": "EXPECT_THROW_MSG", + "ASSERT_THROW": "ASSERT_THROW_MSG", +} + + +class MacroReplacement(Check): + """Flag invocations of a banned macro and suggest its replacement.""" + + id = "macroReplacement" + + def __init__(self) -> None: + """Initialize with the built-in default macro-to-replacement mapping.""" + self.macro_to_replacement: dict[str, str] = dict( + _DEFAULT_MACRO_TO_REPLACEMENT + ) + + def configure(self, config: dict) -> None: + """Replace the default macro-to-replacement mapping from the TOML config. + + Parameters + ---------- + config: dict + Expected shape: + ``{"macro_to_replacement": {"OLD_MACRO": "NEW_MACRO", ...}}``. + When ``macro_to_replacement`` is absent, the built-in default + mapping is kept unchanged. + + Raises + ------ + ConfigError + If ``macro_to_replacement`` is present but has an invalid shape. + + """ + if "macro_to_replacement" not in config: + return + raw = config["macro_to_replacement"] + if not isinstance(raw, dict) or not all( + isinstance(k, str) and isinstance(v, str) for k, v in raw.items() + ): + msg = ( + "macroReplacement: 'macro_to_replacement' must be a table of " + "string -> string mappings" + ) + raise ConfigError(msg) + self.macro_to_replacement = dict(raw) + + def visit(self, cursor: clang.Cursor, filename: str) -> list[Diagnostic]: + """Flag the cursor if it is an invocation of a banned macro. + + Parameters + ---------- + cursor: clang.Cursor + The AST node currently being visited. + filename: str + Path of the file being checked. + + Returns + ------- + list[Diagnostic] + A single diagnostic if `cursor` is a banned macro invocation, + otherwise an empty list. + + """ + if cursor.kind != clang.CursorKind.MACRO_INSTANTIATION: + return [] + + name = cursor.spelling + replacement = self.macro_to_replacement.get(name) + if replacement is None: + return [] + + loc = cursor.location + return [ + Diagnostic( + file=filename, + line=loc.line, + column=loc.column, + message=f"do not use '{name}' — use '{replacement}' instead", + check_id=self.id, + severity="style", + ) + ] diff --git a/src/devops/cpp/ast/registry.py b/src/devops/cpp/ast/registry.py index ebc954e..f3fd06b 100644 --- a/src/devops/cpp/ast/registry.py +++ b/src/devops/cpp/ast/registry.py @@ -10,6 +10,7 @@ import typing from devops.cpp.ast.checks.enforce_param_name_for_type import EnforceParamNameForType +from devops.cpp.ast.checks.macro_replacement import MacroReplacement from devops.cpp.ast.checks.no_global_using import NoGlobalUsing from devops.cpp.ast.checks.no_global_using_enum import NoGlobalUsingEnum from devops.cpp.ast.checks.no_throw_paren import NoThrowParen @@ -24,6 +25,7 @@ # / ast_check_disabled_ids to turn it on or off from the input file. ALL_CHECKS: list[Check] = [ EnforceParamNameForType(), + MacroReplacement(), NoGlobalUsing(), NoGlobalUsingEnum(), NoThrowParen(), diff --git a/tests/cpp/ast/test_macro_replacement.py b/tests/cpp/ast/test_macro_replacement.py new file mode 100644 index 0000000..565785e --- /dev/null +++ b/tests/cpp/ast/test_macro_replacement.py @@ -0,0 +1,120 @@ +"""Tests for the MacroReplacement AST check.""" + +from __future__ import annotations + +import typing + +import pytest + +from devops.config.base import ConfigError +from devops.cpp.ast.checks.macro_replacement import MacroReplacement +from devops.cpp.ast.engine import run_ast_checks + +if typing.TYPE_CHECKING: + from pathlib import Path + +pytest.importorskip("clang.cindex") + +_ARGS = ["-std=c++17"] + +_MACRO_DEFS = ( + "#define EXPECT_THROW(stmt, ex) do { stmt; } while(0)\n" + "#define EXPECT_THROW_MSG(stmt, ex, msg) do { stmt; } while(0)\n" + "#define ASSERT_THROW(stmt, ex) do { stmt; } while(0)\n" + "#define ASSERT_THROW_MSG(stmt, ex, msg) do { stmt; } while(0)\n" +) + + +def _diags( + code: str, tmp_path: Path, check: MacroReplacement | None = None +) -> list[str]: + p = tmp_path / "test.cpp" + p.write_text(code) + checks = [check or MacroReplacement()] + return [d.message for d in run_ast_checks(p, code, _ARGS, checks=checks)] + + +class TestMacroReplacementDefaults: + """The built-in default mapping flags EXPECT_THROW/ASSERT_THROW.""" + + def test_expect_throw_flagged(self, tmp_path: Path) -> None: + """Test expect throw flagged.""" + code = _MACRO_DEFS + "void f() { EXPECT_THROW(throw 1, int); }\n" + diags = _diags(code, tmp_path) + assert len(diags) == 1 + assert "EXPECT_THROW" in diags[0] + assert "EXPECT_THROW_MSG" in diags[0] + + def test_assert_throw_flagged(self, tmp_path: Path) -> None: + """Test assert throw flagged.""" + code = _MACRO_DEFS + "void f() { ASSERT_THROW(throw 1, int); }\n" + diags = _diags(code, tmp_path) + assert len(diags) == 1 + assert "ASSERT_THROW" in diags[0] + assert "ASSERT_THROW_MSG" in diags[0] + + def test_msg_variants_allowed(self, tmp_path: Path) -> None: + """Test msg variants allowed.""" + code = ( + _MACRO_DEFS + + 'void f() { EXPECT_THROW_MSG(throw 1, int, "m"); ' + + 'ASSERT_THROW_MSG(throw 1, int, "m"); }\n' + ) + assert _diags(code, tmp_path) == [] + + def test_macro_defined_in_included_header_still_flagged( + self, tmp_path: Path + ) -> None: + """Test macro defined in included header still flagged.""" + header = tmp_path / "gtest_stub.hpp" + header.write_text(_MACRO_DEFS) + code = '#include "gtest_stub.hpp"\nvoid f() { EXPECT_THROW(throw 1, int); }\n' + diags = _diags(code, tmp_path) + assert len(diags) == 1 + assert "EXPECT_THROW" in diags[0] + + def test_clean_file_produces_no_diagnostics(self, tmp_path: Path) -> None: + """Test clean file produces no diagnostics.""" + code = _MACRO_DEFS + "void f() {}\n" + assert _diags(code, tmp_path) == [] + + +class TestMacroReplacementCheckId: + """The check id is correct.""" + + def test_check_id(self) -> None: + """Test check id.""" + assert MacroReplacement().id == "macroReplacement" + + +class TestMacroReplacementConfig: + """macro_to_replacement replaces the built-in default mapping.""" + + def test_custom_mapping_replaces_default(self, tmp_path: Path) -> None: + """Test custom mapping replaces default.""" + check = MacroReplacement() + check.configure({"macro_to_replacement": {"EXPECT_EQ": "EXPECT_EQ_MSG"}}) + code = ( + "#define EXPECT_THROW(a, b) do {} while(0)\n" + "#define EXPECT_EQ(a, b) do {} while(0)\n" + "void f() { EXPECT_THROW(1, 2); EXPECT_EQ(1, 2); }\n" + ) + diags = _diags(code, tmp_path, check) + assert len(diags) == 1 + assert "EXPECT_EQ" in diags[0] + + def test_missing_key_keeps_default_mapping(self, tmp_path: Path) -> None: + """Test missing key keeps default mapping.""" + check = MacroReplacement() + check.configure({}) + code = _MACRO_DEFS + "void f() { EXPECT_THROW(throw 1, int); }\n" + diags = _diags(code, tmp_path, check) + assert len(diags) == 1 + + @pytest.mark.parametrize( + "bad", [["not", "a", "dict"], {"EXPECT_THROW": 1}, {1: "EXPECT_THROW_MSG"}] + ) + def test_invalid_config_raises(self, bad: object) -> None: + """Test invalid config raises.""" + with pytest.raises(ConfigError, match="macro_to_replacement"): + MacroReplacement().configure({"macro_to_replacement": bad}) From 5471530f26188aa0ebdefd958f1cd7523cfb3d5c Mon Sep 17 00:00:00 2001 From: Jakob Gamper <97gamjak@gmail.com> Date: Sun, 27 Sep 2026 13:50:09 +0200 Subject: [PATCH 2/4] Disallow EXPECT_THROW and ASSERT_THROW Fixes #126 --- CHANGELOG.md | 6 +++ src/devops/cpp/ast/engine.py | 62 +++++++++++++++++++++--- tests/cpp/ast/test_engine.py | 93 ++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index becce6a..9139ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ All notable changes to this project will be documented in this file. - Add `macroReplacement` AST check flagging invocations of a banned macro and suggesting its replacement, e.g. disallowing gtest's `EXPECT_THROW`/`ASSERT_THROW` in favor of custom `EXPECT_THROW_MSG`/`ASSERT_THROW_MSG` macros that also require a failure message. Ships with that mapping as a built-in default (no configuration required) and is fully configurable/extensible via `[cpp.ast_check_config.macroReplacement].macro_to_replacement`. Detection matches the macro name exactly and works whether the macro is defined in the same file or an included header +### Fixes + +#### CPP Rules + +- Fix a libclang crash (`astParseError` with no useful diagnostic) when parsing a file whose compile args force-include a PCH header via the `-include ` compiler flag (as kept by the earlier `-include`-handling fix). Pip's libclang build has been observed to hard-crash on some real PCH headers when force-included this way — even though the exact same header content parses cleanly as an ordinary `#include`, and even though the project's own real compiler accepts the identical flag without issue. `-include ` is now stripped from the compiler args and instead turned into a real `#include` line ahead of the checked file in a synthetic wrapper (the same technique already used for header checks), which avoids the crash while preserving the checked file's own path and line numbers exactly + ## [0.4.2](https://github.com/repo/owner/releases/tag/0.4.2) - 2026-09-26 diff --git a/src/devops/cpp/ast/engine.py b/src/devops/cpp/ast/engine.py index 8571347..a32631b 100644 --- a/src/devops/cpp/ast/engine.py +++ b/src/devops/cpp/ast/engine.py @@ -120,6 +120,45 @@ def _with_fallback_resource_dir(args: list[str]) -> list[str]: return _with_auto_resource_dir(args, clang_exe) +def _extract_plain_include(compile_args: list[str]) -> tuple[list[str], str | None]: + """Pull a bare ``-include `` pair out of `compile_args`, if present. + + `rule.py` normalizes any force-include it decides to keep (CMake PCH + headers, in both GCC's bare and Clang's ``-Xclang``-wrapped spellings) + to exactly this bare pair, so that's the only shape handled here. + + Passing that file to libclang as the ``-include`` compiler flag is + avoided: pip's libclang build has been observed to hard-crash + (``TranslationUnitLoadError``, no diagnostics at all) on some real PCH + headers when force-included this way, even though the exact same + header parses cleanly as an ordinary ``#include`` — and even though + the project's own real compiler parses the identical flag with no + problem. The caller turns the extracted file into a real ``#include`` + line instead (see `run_ast_checks`), which sidesteps the crash and + parses correctly. + + Parameters + ---------- + compile_args: list[str] + The compile args to search. + + Returns + ------- + tuple[list[str], str | None] + `compile_args` with the ``-include `` pair removed (or + unchanged, if none was found) and the extracted file path (or + None). + + """ + if "-include" not in compile_args: + return compile_args, None + idx = compile_args.index("-include") + file_arg = compile_args[idx + 1] if idx + 1 < len(compile_args) else None + if file_arg is None: + return compile_args, None + return compile_args[:idx] + compile_args[idx + 2 :], file_arg + + def run_ast_checks( path: Path, content: str, @@ -152,6 +191,7 @@ def run_ast_checks( """ checks = ALL_CHECKS if checks is None else checks compile_args = _with_fallback_resource_dir(compile_args) + compile_args, prelude_include = _extract_plain_include(compile_args) # Use the display/diagnostic path as-is; resolve to absolute for libclang # so that unsaved-file lookup and AST node locations are consistent. @@ -166,15 +206,23 @@ def run_ast_checks( parse_options = clang.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD index = clang.Index.create() - if is_header: - # Parse a virtual .cpp wrapper that #includes the header so that - # libclang gets a proper translation-unit context. Parsing a header - # directly often causes TranslationUnitLoadError because libclang - # expects a complete translation unit as its entry point. + if is_header or prelude_include is not None: + # Parse a virtual .cpp wrapper that #includes the file(s) so that + # libclang gets a proper translation-unit context, rather than + # passing them as compiler flags. For a header, parsing it directly + # often causes TranslationUnitLoadError because libclang expects a + # complete translation unit as its entry point. For a force-included + # PCH header (`prelude_include`), passing it via the `-include` + # compiler flag instead of an ordinary #include has been observed to + # crash libclang outright on some real headers (see + # `_extract_plain_include`). Either way, the real file is registered + # under its own real path with its own unmodified content, so + # per-file line numbers and locations are unaffected by the wrapper. # Use absolute paths so libclang's internal path resolution can match # our unsaved-file entries (it normalises to absolute before lookup). - wrapper_name = str(Path.cwd() / "__devops_ast_header_check__.cpp") - wrapper_content = f'#include "{filename_abs}"\n' + wrapper_name = str(Path.cwd() / "__devops_ast_wrapper__.cpp") + prelude = f'#include "{prelude_include}"\n' if prelude_include else "" + wrapper_content = f'{prelude}#include "{filename_abs}"\n' unsaved = [(filename_abs, content), (wrapper_name, wrapper_content)] parse_name = wrapper_name filter_name = filename_abs diff --git a/tests/cpp/ast/test_engine.py b/tests/cpp/ast/test_engine.py index 2e18d88..e77ee21 100644 --- a/tests/cpp/ast/test_engine.py +++ b/tests/cpp/ast/test_engine.py @@ -8,6 +8,7 @@ from devops.cpp.ast.checks.no_global_using import NoGlobalUsing from devops.cpp.ast.engine import ( + _extract_plain_include, _has_resource_dir, _with_auto_resource_dir, _with_fallback_resource_dir, @@ -82,3 +83,95 @@ def test_with_fallback_resource_dir_leaves_existing_untouched(self) -> None: """Test with fallback resource dir leaves existing untouched.""" args = ["-resource-dir=/already/set"] assert _with_fallback_resource_dir(args) == args + + +class TestExtractPlainInclude: + """Unit tests for pulling a bare -include out of compile args.""" + + def test_extracts_flag_and_file(self) -> None: + """Test extracts flag and file.""" + args, file = _extract_plain_include(["-std=c++17", "-include", "/pch.hxx"]) + assert file == "/pch.hxx" + assert args == ["-std=c++17"] + + def test_absent_returns_unchanged_args_and_none(self) -> None: + """Test absent returns unchanged args and none.""" + args = ["-std=c++17", "-Wall"] + result_args, file = _extract_plain_include(args) + assert file is None + assert result_args == args + + def test_dangling_include_with_no_file_is_a_noop(self) -> None: + """A trailing -include with nothing after it is left alone, not crashed on.""" + args = ["-std=c++17", "-include"] + result_args, file = _extract_plain_include(args) + assert file is None + assert result_args == args + + +class TestPreludeIncludeWrapping: + """A bare -include is routed through a wrapper #include, not the flag. + + Regression coverage for a real bug: pip's libclang build was observed to + hard-crash (TranslationUnitLoadError, no diagnostics at all) parsing some + real PCH headers when force-included via the `-include` compiler flag, + even though the identical header content parses cleanly as an ordinary + `#include`, and even though the project's own real compiler parses the + same `-include` flag with no problem at all. The engine now strips a + bare `-include ` out of compile_args and instead prepends a real + `#include` line ahead of the checked file in a synthetic wrapper — same + as the existing header-check wrapper technique — which avoids the crash. + """ + + def test_prelude_include_is_processed_and_does_not_crash( + self, tmp_path: Path + ) -> None: + """Test prelude include is processed and does not crash.""" + prelude = tmp_path / "prelude.hpp" + prelude.write_text("using PreludeInt = int;\n") + + p = tmp_path / "test.cpp" + code = "PreludeInt x = 0;\nvoid f() { (void)x; }\n" + p.write_text(code) + + diags = run_ast_checks( + p, code, ["-std=c++17", "-include", str(prelude)], checks=[] + ) + assert diags == [] + + def test_diagnostics_keep_the_real_file_and_original_line_numbers( + self, tmp_path: Path + ) -> None: + """Line numbers/paths for the real file are unaffected by the wrapper.""" + prelude = tmp_path / "prelude.hpp" + prelude.write_text("namespace unrelated {}\n") + + p = tmp_path / "test.cpp" + code = "namespace ns1 {}\nusing namespace ns1;\n" + p.write_text(code) + + diags = run_ast_checks( + p, + code, + ["-std=c++17", "-include", str(prelude)], + checks=[NoGlobalUsing()], + ) + assert len(diags) == 1 + assert diags[0].file == str(p) + assert diags[0].line == 2 + + def test_missing_prelude_file_reports_ast_parse_error_not_a_crash( + self, tmp_path: Path + ) -> None: + """A prelude file that doesn't exist fails as a normal diagnostic.""" + p = tmp_path / "test.cpp" + code = "void f() {}\n" + p.write_text(code) + + diags = run_ast_checks( + p, + code, + ["-std=c++17", "-include", str(tmp_path / "does_not_exist.hpp")], + checks=[], + ) + assert any(d.check_id == "astParseError" for d in diags) From 26713bf5c61d3bbf19fd1ae6b8b780d52a52fd3d Mon Sep 17 00:00:00 2001 From: Jakob Gamper <97gamjak@gmail.com> Date: Sun, 27 Sep 2026 17:58:35 +0200 Subject: [PATCH 3/4] Disallow EXPECT_THROW and ASSERT_THROW Fixes #126 --- .vscode/settings.json | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 60cb032..8078123 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,34 @@ "levelname", "MSTD", "unittests" - ] + ], + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#ab307e", + "activityBar.activeBorder": "#e7e7e7", + "activityBar.background": "#ab307e", + "activityBar.foreground": "#e7e7e7", + "activityBar.inactiveForeground": "#e7e7e799", + "activityBarBadge.background": "#25320e", + "activityBarBadge.foreground": "#e7e7e7", + "activityBarTop.activeBackground": "#ab307e", + "activityBarTop.activeBorder": "#e7e7e7", + "activityBarTop.background": "#ab307e", + "activityBarTop.foreground": "#e7e7e7", + "activityBarTop.inactiveForeground": "#e7e7e799", + "commandCenter.border": "#e7e7e799", + "commandCenter.foreground": "#e7e7e7", + "sash.hoverBorder": "#ab307e", + "statusBar.background": "#832561", + "statusBar.debuggingBackground": "#832561", + "statusBar.debuggingForeground": "#e7e7e7", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#ab307e", + "statusBarItem.remoteBackground": "#121907", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#832561", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#83256199", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.remoteColor": "#832561" } From f21005a60c70c689cbee1fbf16211ca20760b7ca Mon Sep 17 00:00:00 2001 From: Jakob Gamper <97gamjak@gmail.com> Date: Sun, 27 Sep 2026 17:58:51 +0200 Subject: [PATCH 4/4] Disallow EXPECT_THROW and ASSERT_THROW Fixes #126 --- src/devops/cpp/ast/checks/macro_replacement.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/devops/cpp/ast/checks/macro_replacement.py b/src/devops/cpp/ast/checks/macro_replacement.py index 58d5ee2..19f5db8 100644 --- a/src/devops/cpp/ast/checks/macro_replacement.py +++ b/src/devops/cpp/ast/checks/macro_replacement.py @@ -51,9 +51,7 @@ class MacroReplacement(Check): def __init__(self) -> None: """Initialize with the built-in default macro-to-replacement mapping.""" - self.macro_to_replacement: dict[str, str] = dict( - _DEFAULT_MACRO_TO_REPLACEMENT - ) + self.macro_to_replacement: dict[str, str] = dict(_DEFAULT_MACRO_TO_REPLACEMENT) def configure(self, config: dict) -> None: """Replace the default macro-to-replacement mapping from the TOML config.