Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

## Next Release

### Fixes

#### CPP Rules

- Fix a bug in `compile_commands.json` arg filtering where a bare `-include <file>` (as GCC/CMake's `target_precompile_headers` emits, unlike Clang's `-Xclang -include -Xclang <file>`) had its file argument silently dropped by the positional-source-file heuristic, leaving a dangling `-include` that swallowed the next unrelated flag as its filename and caused a fatal parse error. `-include` (bare or `-Xclang`-wrapped, including `-include-pch`) is now always dropped together with its file argument, since a force-included PCH header may not exist for every cmake target

<!-- insertion marker -->
## [0.4.0](https://github.com/repo/owner/releases/tag/0.4.0) - 2026-09-26

Expand Down
52 changes: 39 additions & 13 deletions src/devops/cpp/ast/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@


# Flags that are not useful to libclang and whose following argument (if any)
# should also be dropped.
_SKIP_WITH_ARG = frozenset(("-o", "-MF", "-MT", "-MQ"))
# should also be dropped. -include is dropped entirely (not just filtered by
# the source-extension check below) because a force-included PCH header may
# not exist for every cmake target and would otherwise cause a hard parse
# failure; see the -Xclang handling below for the Clang-specific spelling of
# the same flag.
_SKIP_WITH_ARG = frozenset(("-o", "-MF", "-MT", "-MQ", "-include"))
# Flags that are not useful but take no following argument.
_SKIP_ALONE = frozenset(
(
Expand All @@ -36,6 +40,38 @@
_SOURCE_EXTENSIONS = (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".hh", ".h")


def _consume_xclang(it: typing.Iterator[str], result: list[str]) -> None:
"""Handle the token(s) after an ``-Xclang`` already consumed from `it`.

Drops ``-Xclang -include-pch`` / ``-Xclang -include`` and their file
argument entirely, same as the bare ``-include`` case in
`_args_from_compile_commands`: a force-included PCH header may not exist
for every cmake target and would otherwise cause a hard parse failure.
That file argument may itself be wrapped in another ``-Xclang``
(``-Xclang -include-pch -Xclang <file>``) or bare
(``-Xclang -include-pch <file>``) depending on the CMake/Clang version,
so this peeks one token rather than assuming a fixed shape. Every other
``-Xclang <frontend-arg>`` pair is appended to `result` as-is.

Parameters
----------
it: typing.Iterator[str]
The shared argument iterator, positioned right after ``-Xclang``.
result: list[str]
The filtered-args list being built; appended to in place.

"""
xclang_arg = next(it, None)
if xclang_arg in ("-include-pch", "-include"):
following = next(it, None) # the file, bare or another -Xclang
if following == "-Xclang":
next(it, None) # the file itself, wrapped
return
if xclang_arg is not None:
result.append("-Xclang")
result.append(xclang_arg)


def _args_from_compile_commands(
db: clang.CompilationDatabase, path: Path
) -> list[str] | None:
Expand All @@ -59,18 +95,8 @@ def _args_from_compile_commands(
# Skip source / header files passed as positional arguments.
if not arg.startswith("-") and arg.endswith(_SOURCE_EXTENSIONS):
continue
# Handle -Xclang <frontend-arg> pairs. Drop -include-pch entirely
# (the .gch file may not exist for every cmake target and causes a
# hard parse failure). All other -Xclang pairs are kept as-is.
if arg == "-Xclang":
xclang_arg = next(it, None)
if xclang_arg == "-include-pch":
next(it, None) # skip following -Xclang
next(it, None) # skip the .gch file path
continue
if xclang_arg is not None:
result.append(arg)
result.append(xclang_arg)
_consume_xclang(it, result)
continue
result.append(arg)
# Clang/libclang compatibility: silently ignore GCC-only flags that
Expand Down
105 changes: 104 additions & 1 deletion tests/cpp/ast/test_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

from __future__ import annotations

import json
import typing

import clang.cindex as clang
import pytest

from devops.cpp.ast.rule import ASTChecksRule
from devops.cpp.ast.rule import ASTChecksRule, _args_from_compile_commands
from devops.rules import FileRuleInput, ResultTypeEnum

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -66,3 +68,104 @@ def test_ok_when_path_missing(self) -> None:
result = rule.apply(FileRuleInput(file_content="void foo() {}", path=None))

assert result.value == ResultTypeEnum.Ok


def _write_compile_commands(
tmp_path: Path, directory: Path, file: Path, command: str
) -> None:
"""Write a one-entry compile_commands.json under `directory`."""
(directory / "compile_commands.json").write_text(
json.dumps(
[{"directory": str(tmp_path), "file": str(file), "command": command}]
)
)


class TestArgsFromCompileCommands:
"""`-include`/PCH handling: bare (GCC) and -Xclang-wrapped (Clang) forms.

Regression coverage for a real bug: a bare `-include <pch>.hxx` (as GCC
emits for CMake's target_precompile_headers, unlike Clang's
`-Xclang -include -Xclang <pch>.hxx`) used to have its file argument
silently dropped by the positional-source-file heuristic (it ends in
`.hxx`, one of `_SOURCE_EXTENSIONS`) while the `-include` flag itself
was kept — leaving a dangling `-include` that then swallowed the next
*unrelated* flag in the args list as its filename, producing a fatal
"file not found" libclang parse error for a file that was never
referenced by the project at all.
"""

def test_bare_include_and_its_file_are_both_dropped(self, tmp_path: Path) -> None:
"""Test bare include and its file are both dropped."""
db_dir = tmp_path
cpp_file = tmp_path / "foo.cpp"
cpp_file.write_text("void f() {}\n")
command = (
"/usr/bin/g++-13 -Winvalid-pch -include "
f"{tmp_path}/cmake_pch.hxx -std=c++23 -o foo.o -c {cpp_file}"
)
_write_compile_commands(tmp_path, db_dir, cpp_file, command)

db = clang.CompilationDatabase.fromDirectory(str(db_dir))
args = _args_from_compile_commands(db, cpp_file)

assert args is not None
assert "-include" not in args
assert not any(a.endswith("cmake_pch.hxx") for a in args)
# No flag is left dangling right before our own appended flags.
idx = args.index("-Wno-unknown-warning-option")
assert args[idx - 1] != "-include"

def test_xclang_wrapped_include_pch_is_dropped(self, tmp_path: Path) -> None:
"""Test xclang wrapped include pch is dropped."""
db_dir = tmp_path
cpp_file = tmp_path / "foo.cpp"
cpp_file.write_text("void f() {}\n")
command = (
"/usr/bin/clang++-20 -Xclang -include-pch -Xclang "
f"{tmp_path}/pch.pch -std=c++23 -o foo.o -c {cpp_file}"
)
_write_compile_commands(tmp_path, db_dir, cpp_file, command)

db = clang.CompilationDatabase.fromDirectory(str(db_dir))
args = _args_from_compile_commands(db, cpp_file)

assert args is not None
assert "-include-pch" not in args
assert not any(a.endswith("pch.pch") for a in args)

def test_xclang_wrapped_include_is_dropped(self, tmp_path: Path) -> None:
"""Test xclang wrapped include is dropped (Clang's own -include form)."""
db_dir = tmp_path
cpp_file = tmp_path / "foo.cpp"
cpp_file.write_text("void f() {}\n")
command = (
"/usr/bin/clang++-20 -Xclang -include -Xclang "
f"{tmp_path}/cmake_pch.hxx -std=c++23 -o foo.o -c {cpp_file}"
)
_write_compile_commands(tmp_path, db_dir, cpp_file, command)

db = clang.CompilationDatabase.fromDirectory(str(db_dir))
args = _args_from_compile_commands(db, cpp_file)

assert args is not None
assert "-include" not in args
assert not any(a.endswith("cmake_pch.hxx") for a in args)

def test_other_xclang_pairs_are_preserved(self, tmp_path: Path) -> None:
"""Test other xclang pairs are preserved."""
db_dir = tmp_path
cpp_file = tmp_path / "foo.cpp"
cpp_file.write_text("void f() {}\n")
command = (
"/usr/bin/clang++-20 -Xclang -fsome-frontend-flag "
f"-std=c++23 -o foo.o -c {cpp_file}"
)
_write_compile_commands(tmp_path, db_dir, cpp_file, command)

db = clang.CompilationDatabase.fromDirectory(str(db_dir))
args = _args_from_compile_commands(db, cpp_file)

assert args is not None
assert "-Xclang" in args
assert "-fsome-frontend-flag" in args
Loading