Author: Cyndy Ishida
Date: 2026-07-01T11:53:06-07:00
New Revision: 43424c3e98f6fe07e729b55199bb91ff58579871

URL: 
https://github.com/llvm/llvm-project/commit/43424c3e98f6fe07e729b55199bb91ff58579871
DIFF: 
https://github.com/llvm/llvm-project/commit/43424c3e98f6fe07e729b55199bb91ff58579871.diff

LOG: [clang-scan-deps] Add scan-deps-filter.py test helper to filter full 
output (#206758)

Add a helper script, which projects `clang-scan-deps` experimental-full
JSON down to a chosen set of fields, plus a `%scan-deps-filter` lit
substitution. A bare key (e.g. `file-deps`) matches that key at any
depth. A dotted path (e.g. `modules.command-line`) is anchored from the
document root to disambiguate keys when relevant.

This lets tests assert only on the fields they care about instead of
`CHECK`ing the whole object, which otherwise breaks whenever an
unrelated field is added/modified, and avoids gating emission behind
awkward per-field flags.

Migrate modules-full-by-mod-name.c as a first example.

Assisted-by: Claude Opus 4.8

Added: 
    clang/utils/scan-deps-filter.py

Modified: 
    clang/test/ClangScanDeps/modules-full-by-mod-name.c
    clang/test/lit.cfg.py

Removed: 
    


################################################################################
diff  --git a/clang/test/ClangScanDeps/modules-full-by-mod-name.c 
b/clang/test/ClangScanDeps/modules-full-by-mod-name.c
index 2d2c9acd30011..ee97926b15495 100644
--- a/clang/test/ClangScanDeps/modules-full-by-mod-name.c
+++ b/clang/test/ClangScanDeps/modules-full-by-mod-name.c
@@ -26,7 +26,8 @@ module transitive { header "transitive.h" }
 
 // RUN: sed "s|DIR|%/t|g" %t/cdb.json.template > %t/cdb.json
 // RUN: clang-scan-deps -compilation-database %t/cdb.json -format 
experimental-full -module-names=root > %t/result.json
-// RUN: cat %t/result.json | sed 's:\\\\\?:/:g' | FileCheck -DPREFIX=%/t %s
+// RUN: cat %t/result.json | sed 's:\\\\\?:/:g' \
+// RUN:   | %scan-deps-filter 
--fields=modules.name,modules.file-deps,modules.clang-module-deps | FileCheck 
-DPREFIX=%/t %s
 
 //--- cdb.cc1.json.template
 [{
@@ -37,7 +38,11 @@ module transitive { header "transitive.h" }
 
 // RUN: sed "s|DIR|%/t|g" %t/cdb.cc1.json.template > %t/cdb.cc1.json
 // RUN: clang-scan-deps -compilation-database %t/cdb.cc1.json -format 
experimental-full -module-names=root > %t/result.cc1.json
-// RUN: cat %t/result.cc1.json | sed 's:\\\\\?:/:g' | FileCheck -DPREFIX=%/t %s
+// RUN: cat %t/result.cc1.json | sed 's:\\\\\?:/:g' \
+// RUN:   | %scan-deps-filter 
--fields=modules.name,modules.file-deps,modules.clang-module-deps | FileCheck 
-DPREFIX=%/t %s
+
+// Assert only on the module graph, its file dependencies, and each module's
+// dependencies.
 
 // CHECK:      {
 // CHECK-NEXT:   "modules": [
@@ -48,15 +53,10 @@ module transitive { header "transitive.h" }
 // CHECK-NEXT:           "module-name": "transitive"
 // CHECK-NEXT:         }
 // CHECK-NEXT:       ],
-// CHECK-NEXT:       "clang-modulemap-file": "[[PREFIX]]/module.modulemap",
-// CHECK-NEXT:       "command-line": [
-// CHECK:            ],
-// CHECK-NEXT:       "context-hash": "{{.*}}",
 // CHECK-NEXT:       "file-deps": [
 // CHECK-NEXT:         "[[PREFIX]]/module.modulemap",
 // CHECK-NEXT:         "[[PREFIX]]/direct.h"
 // CHECK-NEXT:       ],
-// CHECK-NEXT:       "link-libraries": [],
 // CHECK-NEXT:       "name": "direct"
 // CHECK-NEXT:     },
 // CHECK-NEXT:     {
@@ -66,31 +66,20 @@ module transitive { header "transitive.h" }
 // CHECK-NEXT:           "module-name": "direct"
 // CHECK-NEXT:         }
 // CHECK-NEXT:       ],
-// CHECK-NEXT:       "clang-modulemap-file": "[[PREFIX]]/module.modulemap",
-// CHECK-NEXT:       "command-line": [
-// CHECK:            ],
-// CHECK-NEXT:       "context-hash": "{{.*}}",
 // CHECK-NEXT:       "file-deps": [
 // CHECK-NEXT:         "[[PREFIX]]/module.modulemap",
 // CHECK-NEXT:         "[[PREFIX]]/root.h",
 // CHECK-NEXT:         "[[PREFIX]]/root/textual.h"
 // CHECK-NEXT:       ],
-// CHECK-NEXT:       "link-libraries": [],
 // CHECK-NEXT:       "name": "root"
 // CHECK-NEXT:     },
 // CHECK-NEXT:     {
 // CHECK-NEXT:       "clang-module-deps": [],
-// CHECK-NEXT:       "clang-modulemap-file": "[[PREFIX]]/module.modulemap",
-// CHECK-NEXT:       "command-line": [
-// CHECK:            ],
-// CHECK-NEXT:       "context-hash": "{{.*}}",
 // CHECK-NEXT:       "file-deps": [
 // CHECK-NEXT:         "[[PREFIX]]/module.modulemap",
 // CHECK-NEXT:         "[[PREFIX]]/transitive.h"
 // CHECK-NEXT:       ],
-// CHECK-NEXT:       "link-libraries": [],
 // CHECK-NEXT:       "name": "transitive"
 // CHECK-NEXT:     }
-// CHECK-NEXT:   ],
-// CHECK-NEXT:   "translation-units": []
+// CHECK-NEXT:   ]
 // CHECK-NEXT: }

diff  --git a/clang/test/lit.cfg.py b/clang/test/lit.cfg.py
index f7b3a77266cb8..5a5e819c9b5a1 100644
--- a/clang/test/lit.cfg.py
+++ b/clang/test/lit.cfg.py
@@ -313,6 +313,17 @@ def have_host_clang_repl_cuda():
     )
 )
 
+config.substitutions.append(
+    (
+        "%scan-deps-filter",
+        '"%s" %s'
+        % (
+            config.python_executable,
+            os.path.join(config.clang_src_dir, "utils", "scan-deps-filter.py"),
+        ),
+    )
+)
+
 # Determine whether the test target is compatible with execution on the host.
 if "aarch64" in config.host_arch:
     config.available_features.add("aarch64-host")

diff  --git a/clang/utils/scan-deps-filter.py b/clang/utils/scan-deps-filter.py
new file mode 100644
index 0000000000000..9c640a133150b
--- /dev/null
+++ b/clang/utils/scan-deps-filter.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+
+"""Filters clang-scan-deps full output down to a chosen set of fields.
+
+clang-scan-deps prints every computed field in its `experimental-full`
+output. Pipe the full output through this script to keep only the fields a
+test cares about, then hand that to FileCheck.
+
+Usage:
+
+  clang-scan-deps -format experimental-full ... \\
+    | scan-deps-filter.py --fields=name,file-deps \\
+    | FileCheck %s
+
+In lit tests the script is invoked through the `%scan-deps-filter`
+substitution rather than by path.
+
+Each --fields entry is one of:
+
+  * a bare key (e.g. `file-deps`), which matches that key wherever it
+    appears at any depth, or
+  * a dotted path (e.g. `modules.command-line`), which matches only that
+    key when reached along the given path from the document root. Every
+    object key on the path must be named. A translation unit's command line,
+    for instance, is at `translation-units.commands.command-line`.
+
+The result is deterministically ordered JSON, in the same key order
+clang-scan-deps emitted.
+"""
+
+import argparse
+import json
+import sys
+
+
+def parse_patterns(fields):
+    """Turn each --fields entry into a list of path segments. A bare key
+    becomes a wildcard-prefixed pattern that matches anywhere; a dotted entry
+    becomes an anchored path from the root."""
+    patterns = []
+    for field in fields:
+        if "." in field:
+            patterns.append(field.split("."))
+        else:
+            # "**" marks a bare key: match its single trailing segment at any
+            # depth (it is not glob-style recursive descent).
+            patterns.append(["**", field])
+    return patterns
+
+
+def matches(keypath, patterns):
+    """Does the key path to the current value (array indices omitted) match
+    any pattern? A bare-key ("**") pattern matches when its trailing segment
+    equals the end of the key path; a dotted pattern must equal the whole
+    path from the root."""
+    for pattern in patterns:
+        if pattern[0] == "**":
+            tail = pattern[1:]
+            if keypath[-len(tail) :] == tail:
+                return True
+        elif keypath == pattern:
+            return True
+    return False
+
+
+def prune(value, keypath, patterns):
+    """Return a copy of `value` keeping only paths that reach a matching key,
+    or None when nothing under `value` matches."""
+    if matches(keypath, patterns):
+        # Selected key: keep its entire subtree verbatim.
+        return value
+    if isinstance(value, dict):
+        result = {}
+        for key, sub in value.items():
+            pruned = prune(sub, keypath + [key], patterns)
+            if pruned is not None:
+                result[key] = pruned
+        return result if result else None
+    if isinstance(value, list):
+        # Arrays are transparent: items keep the enclosing key path.
+        result = [prune(item, keypath, patterns) for item in value]
+        result = [item for item in result if item is not None]
+        return result if result else None
+    # A scalar only survives when reached through a matching key (above).
+    return None
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "input",
+        nargs="?",
+        help="Full dependencies JSON file (defaults to stdin).",
+    )
+    parser.add_argument(
+        "--fields",
+        required=True,
+        help="Comma-separated list of keys or dotted paths to keep.",
+    )
+    args = parser.parse_args()
+
+    fields = [f for f in args.fields.split(",") if f]
+    if not fields:
+        parser.error("--fields requires at least one field name")
+    patterns = parse_patterns(fields)
+
+    with open(args.input) if args.input else sys.stdin as f:
+        data = json.load(f)
+
+    pruned = prune(data, [], patterns)
+    if pruned is None:
+        # Nothing matched; emit an empty object but warn so a mistyped field
+        # name does not silently feed empty input to FileCheck.
+        sys.stderr.write(
+            "scan-deps-filter: warning: no fields matched: %s\n" % 
",".join(fields)
+        )
+        pruned = {}
+
+    json.dump(pruned, sys.stdout, indent=2)
+    sys.stdout.write("\n")
+
+
+if __name__ == "__main__":
+    main()


        
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to