Merge pull request #2739 from jhasse/exclude-validation-edges-from-compdb

compdb: Exclude validation-only edges from compile_commands.json
This commit is contained in:
Jan Niklas Hasse
2026-04-12 15:33:53 +02:00
committed by GitHub
2 changed files with 148 additions and 2 deletions

View File

@@ -1011,6 +1011,18 @@ std::string EvaluateCommandWithRspfile(const Edge* edge,
return command;
}
/// Returns true if this edge's outputs are only used as validation
/// dependencies by other edges, not as regular build inputs.
bool IsValidationOnlyEdge(const Edge* edge) {
for (const Node* output : edge->outputs_) {
if (output->validation_out_edges().empty() ||
!output->out_edges().empty()) {
return false;
}
}
return !edge->outputs_.empty();
}
void PrintCompdbObjectsForEdge(std::string const& directory, const Edge* const edge,
const EvaluateCommandMode eval_mode) {
const auto& command = EvaluateCommandWithRspfile(edge, eval_mode);
@@ -1070,7 +1082,7 @@ int NinjaMain::ToolCompilationDatabase(const Options* options, int argc,
std::string directory = GetWorkingDirectory();
putchar('[');
for (const Edge* edge : state_.edges_) {
if (edge->inputs_.empty())
if (edge->inputs_.empty() || IsValidationOnlyEdge(edge))
continue;
if (argc == 0) {
if (!first) {
@@ -1228,7 +1240,7 @@ void PrintCompdb(std::string const& directory, std::vector<Edge*> const& edges,
bool first = true;
for (const Edge* edge : edges) {
if (edge->is_phony() || edge->inputs_.empty())
if (edge->is_phony() || edge->inputs_.empty() || IsValidationOnlyEdge(edge))
continue;
if (!first)
putchar(',');

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Integration test: compdb should not include validation-only edges."""
import json
import os
import subprocess
import tempfile
import unittest
NINJA_PATH = os.path.abspath("./ninja")
class CompdbValidationTest(unittest.TestCase):
"""Test that 'ninja -t compdb' excludes edges whose outputs are only
used as validation dependencies (|@)."""
def setUp(self):
self.test_dir = tempfile.mkdtemp(prefix="ninja_compdb_test_")
self.original_dir = os.getcwd()
os.chdir(self.test_dir)
def tearDown(self):
os.chdir(self.original_dir)
import shutil
shutil.rmtree(self.test_dir, ignore_errors=True)
def _run_compdb(self, build_ninja, *extra_args):
with open("build.ninja", "w") as f:
f.write(build_ninja)
cmd = [NINJA_PATH, "-t", "compdb"] + list(extra_args)
result = subprocess.run(cmd, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
return json.loads(result.stdout)
def _run_compdb_targets(self, build_ninja, *targets):
with open("build.ninja", "w") as f:
f.write(build_ninja)
cmd = [NINJA_PATH, "-t", "compdb-targets"] + list(targets)
result = subprocess.run(cmd, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
return json.loads(result.stdout)
def test_compdb_excludes_validation_edge(self):
"""A pure validation edge (output only used via |@) must not appear."""
plan = """\
rule cc
command = gcc -c $in -o $out
rule validate
command = python check.py $in
build foo.o : cc foo.c |@ check_foo
build check_foo : validate foo.c
"""
entries = self._run_compdb(plan)
commands = [e["command"] for e in entries]
self.assertTrue(any("gcc" in c for c in commands), "cc edge should be present")
self.assertFalse(
any("check.py" in c for c in commands), "validation edge should be excluded"
)
def test_compdb_with_rule_filter_excludes_validation(self):
"""Filtering by rule name still excludes validation-only edges."""
plan = """\
rule cc
command = gcc -c $in -o $out
rule validate
command = python check.py $in
build foo.o : cc foo.c |@ check_foo
build check_foo : validate foo.c
"""
entries = self._run_compdb(plan, "validate")
self.assertEqual(
entries, [], "validation rule should produce no compdb entries"
)
def test_compdb_keeps_non_validation_edge(self):
"""Edges whose outputs are regular build inputs must still appear."""
plan = """\
rule cc
command = gcc -c $in -o $out
rule link
command = gcc $in -o $out
build foo.o : cc foo.c
build bar.o : cc bar.c
build prog : link foo.o bar.o
"""
entries = self._run_compdb(plan)
outputs = {e["output"] for e in entries}
self.assertIn("foo.o", outputs)
self.assertIn("bar.o", outputs)
self.assertIn("prog", outputs)
def test_compdb_targets_excludes_validation_edge(self):
"""compdb-targets should also exclude validation-only edges."""
plan = """\
rule cc
command = gcc -c $in -o $out
rule validate
command = python check.py $in
build foo.o : cc foo.c |@ check_foo
build check_foo : validate foo.c
"""
entries = self._run_compdb_targets(plan, "foo.o")
commands = [e["command"] for e in entries]
self.assertTrue(any("gcc" in c for c in commands))
self.assertFalse(any("check.py" in c for c in commands))
def test_compdb_mixed_validation_and_input(self):
"""An edge whose output is used as both validation and regular input
should still be included."""
plan = """\
rule cc
command = gcc -c $in -o $out
rule link
command = gcc $in -o $out
build foo.o : cc foo.c |@ bar.o
build bar.o : cc bar.c
build prog : link foo.o bar.o
"""
entries = self._run_compdb(plan)
outputs = {e["output"] for e in entries}
self.assertIn(
"bar.o", outputs, "bar.o is also a regular input so it must be included"
)
if __name__ == "__main__":
unittest.main()