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
