blob: bd6774e8429392601aaa98a0ae13532715c96d8f [file]
#!/usr/bin/env python3
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
import argparse
import json
import os
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import List, Optional
"""
Run idt on the headers changed in a PR and (optionally) post the resulting
LLVM_ABI annotation diff as a PR comment.
The heavy lifting (per-category clang flags, system-header-prefix isolation,
output filtering, the `hasBody` / out-of-line distinction) lives in `idt`
itself (the standalone `compnerd/ids` tool, built separately and passed
via `--idt-path`). This script is a thin orchestrator: it picks which
headers to check, finds a translation unit for each (since LLVM's compile
database doesn't list headers), and forwards the list to idt batched by
category.
Usage as a git pre-commit hook:
ln -s $(pwd)/llvm/utils/git/ids-check-helper.py .git/hooks/pre-commit
Environment variables `IDT_PATH` and `COMPILE_COMMANDS_PATH` may be used in
place of the matching command-line arguments.
"""
# Symbols that are ignored by the idt check.
IGNORED_SYMBOLS = [
# The entry point for the pass plugin. It is meant to be defined in another
# library and is not exported by LLVM.
"llvmGetPassPluginInfo",
]
# Headers we skip outright. Each entry is matched against the changed-file
# path with `startswith`, so it can be either a directory prefix (trailing /)
# or a specific file path.
SKIP_HEADERS = [
# These are not LLVM component libraries.
"llvm/include/llvm/HTTP/",
"llvm/include/llvm/Debuginfod/",
# These are meant to be included via `Pass.h`, not directly.
"llvm/include/llvm/PassAnalysisSupport.h",
"llvm/include/llvm/PassSupport.h",
# Test-only headers.
"llvm/include/llvm/Testing/",
# OProfile JIT bridge, disabled by default.
"llvm/include/llvm/ExecutionEngine/OProfileWrapper.h",
"llvm/include/llvm/ExecutionEngine/RuntimeDyld.h",
# PDB DIA requires Windows ATL (atlbase.h), unbuildable on Linux runners.
"llvm/include/llvm/DebugInfo/PDB/DIA/",
"llvm/include/llvm/DebugInfo/PDB/ConcreteSymbolEnumerator.h",
# No in-tree non-tools/non-target source #includes these headers, so we
# can't run idt on a TU that pulls them in.
"llvm/include/llvm/ExecutionEngine/Interpreter.h",
"llvm/include/llvm/Support/DebugLog.h",
"llvm/include/llvm/Support/TargetSelect.h",
# zOS-specific shims.
"llvm/include/llvm/Support/AutoConvert.h",
# `class LLVM_ABI DagInit` (and similar in this file) inherit from
# `TrailingObjects<DagInit, T1, T2>` with multiple trailing types.
# MSVC `__declspec(dllexport)` on the class forces instantiation of
# all members including `getTrailingObjects()` (no-arg), which has a
# `static_assert(sizeof...(TrailingTys) == 1, ...)` in its body. The
# build then fails with C2338. Until `TrailingObjects.h` is updated
# to gate the no-arg overload via SFINAE / requires-clause, skip the
# whole header.
"llvm/include/llvm/TableGen/Record.h",
# Pimpl classes annotated `LLVM_ABI` that hold `std::unique_ptr<T>`
# of a forward-declared T defined only in the matching .cpp. MSVC's
# class-level `__declspec(dllexport)` forces the virtual destructor
# to be emitted inline as part of the exported vtable, which requires
# T to be complete in every consuming TU. Pulling T's definition into
# the public header would leak the implementation, so the right move
# is to keep these out of the bulk pass. Long-term: teach idt to
# detect this pattern (unique_ptr<forward-decl> + virtual destructor)
# and annotate individual methods instead of the class.
"llvm/include/llvm/MC/MCWinCOFFObjectWriter.h",
# Wraps the Visual Studio "Setup Configuration" COM API. The
# declarations rely on `EXTERN_C` / `MAXUINT` from <windows.h>, which
# idt's standalone parse doesn't pull in. Same long-term fix shape as
# `AutoConvert.h` (zOS): guard the annotations in-source, or include
# the prerequisite header. Skip for now.
"llvm/include/llvm/WindowsDriver/MSVCSetupApi.h",
]
# Manual header -> source mappings for headers that don't fit the conventional
# `llvm/include/llvm/<Subsystem>/<Bar>.h` -> `llvm/lib/<Subsystem>/<Bar>.cpp`
# layout. Used when neither the direct mapping nor the same-subdirectory
# grep fallback finds a workable source.
HEADER_SOURCE_OVERRIDES = {
# LLVM-C headers
"llvm/include/llvm-c/Analysis.h": "llvm/lib/Analysis/Analysis.cpp",
"llvm/include/llvm-c/BitReader.h": "llvm/lib/Bitcode/Reader/BitReader.cpp",
"llvm/include/llvm-c/BitWriter.h": "llvm/lib/Bitcode/Writer/BitWriter.cpp",
"llvm/include/llvm-c/Comdat.h": "llvm/lib/IR/Comdat.cpp",
"llvm/include/llvm-c/Core.h": "llvm/lib/IR/Core.cpp",
"llvm/include/llvm-c/DebugInfo.h": "llvm/lib/IR/DebugInfo.cpp",
"llvm/include/llvm-c/Disassembler.h": "llvm/lib/MC/MCDisassembler/Disassembler.cpp",
"llvm/include/llvm-c/ErrorHandling.h": "llvm/lib/Support/ErrorHandling.cpp",
"llvm/include/llvm-c/ExecutionEngine.h": "llvm/lib/ExecutionEngine/ExecutionEngineBindings.cpp",
"llvm/include/llvm-c/IRReader.h": "llvm/lib/IRReader/IRReader.cpp",
"llvm/include/llvm-c/LLJIT.h": "llvm/lib/ExecutionEngine/Orc/OrcV2CBindings.cpp",
"llvm/include/llvm-c/LLJITUtils.h": "llvm/lib/ExecutionEngine/Orc/Debugging/LLJITUtilsCBindings.cpp",
"llvm/include/llvm-c/Linker.h": "llvm/lib/Linker/LinkModules.cpp",
"llvm/include/llvm-c/Object.h": "llvm/lib/Object/Object.cpp",
"llvm/include/llvm-c/Orc.h": "llvm/lib/ExecutionEngine/Orc/OrcV2CBindings.cpp",
"llvm/include/llvm-c/OrcEE.h": "llvm/lib/ExecutionEngine/Orc/OrcV2CBindings.cpp",
"llvm/include/llvm-c/Remarks.h": "llvm/lib/Remarks/RemarkParser.cpp",
"llvm/include/llvm-c/Support.h": "llvm/lib/Support/CommandLine.cpp",
"llvm/include/llvm-c/Target.h": "llvm/lib/Target/Target.cpp",
"llvm/include/llvm-c/TargetMachine.h": "llvm/lib/Target/TargetMachineC.cpp",
"llvm/include/llvm-c/Transforms/PassBuilder.h": "llvm/lib/Passes/PassBuilderBindings.cpp",
"llvm/include/llvm-c/Types.h": "llvm/lib/IR/Core.cpp",
"llvm/include/llvm-c/lto.h": "llvm/tools/lto/lto.cpp",
# Top-level llvm/ headers
"llvm/include/llvm/Pass.h": "llvm/lib/IR/Pass.cpp",
"llvm/include/llvm/PassAnalysisSupport.h": "llvm/lib/IR/Pass.cpp",
"llvm/include/llvm/PassRegistry.h": "llvm/lib/IR/PassRegistry.cpp",
"llvm/include/llvm/PassSupport.h": "llvm/lib/IR/Pass.cpp",
"llvm/include/llvm/InitializePasses.h": "llvm/lib/Analysis/Analysis.cpp",
"llvm/include/llvm/LinkAllIR.h": "llvm/lib/IR/Core.cpp",
"llvm/include/llvm/LinkAllPasses.h": "llvm/lib/IR/Pass.cpp",
# Headers under llvm/include/llvm/Target/ whose same-subdir grep fallback
# picks per-target sources (e.g. X86, AArch64). Redirect to lib/CodeGen/.
"llvm/include/llvm/Target/CGPassBuilderOption.h": "llvm/lib/CodeGen/TargetPassConfig.cpp",
"llvm/include/llvm/Target/TargetOptions.h": "llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp",
# Headers pulled in transitively by a small set of "umbrella" sources.
"llvm/include/llvm/ADT/ilist_node_base.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm/Analysis/SimplifyQuery.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm/CodeGenTypes/MachineValueType.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm/IR/Analysis.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm/IR/ConstantFolder.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm/IR/FMF.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm/IR/GenericFloatingPointPredicateUtils.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm/IR/IRBuilderFolder.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm/Support/Recycler.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm-c/Error.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
"llvm/include/llvm-c/Visibility.h": "llvm/lib/CodeGen/CodeGenPrepare.cpp",
# DTLTO drags in Any/LTO/FormatVariadic.
"llvm/include/llvm/ADT/Any.h": "llvm/lib/DTLTO/DTLTO.cpp",
"llvm/include/llvm/LTO/Config.h": "llvm/lib/DTLTO/DTLTO.cpp",
"llvm/include/llvm/Support/FormatVariadicDetails.h": "llvm/lib/DTLTO/DTLTO.cpp",
# Orc debugging / executor-side helpers.
"llvm/include/llvm/DebugInfo/DWARF/LowLevel/DWARFDataExtractorSimple.h": "llvm/lib/ExecutionEngine/Orc/Debugging/DebugInfoSupport.cpp",
"llvm/include/llvm/ExecutionEngine/Orc/MaterializationUnit.h": "llvm/lib/ExecutionEngine/Orc/Debugging/DebugInfoSupport.cpp",
"llvm/include/llvm/ExecutionEngine/Orc/TargetProcess/ExecutorBootstrapService.h": "llvm/lib/ExecutionEngine/Orc/TargetProcess/ExecutorSharedMemoryMapperService.cpp",
"llvm/include/llvm-c/blake3.h": "llvm/lib/ExecutionEngine/Orc/TargetProcess/ExecutorSharedMemoryMapperService.cpp",
"llvm/include/llvm/CodeGen/AsmPrinterHandler.h": "llvm/lib/CodeGen/AsmPrinter/DebugHandlerBase.cpp",
}
# Include subdir -> lib subdir remap for cases where the names diverge.
# ADT has no `llvm/lib/ADT/`; its few non-inline implementations live under
# `llvm/lib/Support/`.
INCLUDE_TO_LIB_SUBDIR = {
"ADT": "Support",
}
# Categories: each header path-fragment maps to (category-name, export-macro).
# Order matters: more specific fragments first (Demangle before generic LLVM).
CATEGORIES = [
("Demangle", "llvm/Demangle/", "DEMANGLE_ABI"),
("LLVM-C", "llvm-c/", "LLVM_C_ABI"),
("LLVM", "llvm/", "LLVM_ABI"),
]
EXPORT_MACRO = {name: macro for name, _fragment, macro in CATEGORIES}
# Predefines applied to every idt invocation (via `--extra-arg`).
#
# Why these are required: idt detects an existing annotation by inspecting
# clang's parsed attributes (`DLLExportAttr` / `DLLImportAttr` /
# `VisibilityAttr`). In a static LLVM build, the donor source's compile
# command carries `-DLLVM_BUILD_STATIC`, which collapses the macro guards
# in `Compiler.h` / `DemangleConfig.h` / `llvm-c/Visibility.h` so that
# `LLVM_ABI` / `DEMANGLE_ABI` / `LLVM_C_ABI` all expand to *empty*. The
# parser then sees no attribute on already-annotated declarations, and
# idt happily prepends another copy of the macro on top.
#
# The fix is to make those macros expand to a real attribute regardless
# of the donor's static-vs-dylib choice:
# - `-ULLVM_BUILD_STATIC` removes the kill-switch.
# - `-DLLVM_ENABLE_LLVM_EXPORT_ANNOTATIONS` opens the LLVM_ABI /
# DEMANGLE_ABI gates.
# - `-DLLVM_ENABLE_LLVM_C_EXPORT_ANNOTATIONS` opens the LLVM_C_ABI gate.
# - `-DLLVM_EXPORTS` makes the cascade pick `dllexport` over `dllimport`
# (either would suffice for detection, dllexport is the symmetric
# choice for a "we're inside the library" parse).
EXTRA_ARGS_COMMON = [
"-ULLVM_BUILD_STATIC",
"-DLLVM_ENABLE_LLVM_EXPORT_ANNOTATIONS",
"-DLLVM_ENABLE_LLVM_C_EXPORT_ANNOTATIONS",
"-DLLVM_EXPORTS",
]
def categorize_header(path: str) -> Optional[str]:
"""Return the category name (LLVM/LLVM-C/Demangle) for a header, or None
if the path doesn't fall under any known category."""
for name, fragment, _macro in CATEGORIES:
if fragment in path:
return name
return None
def find_source_for_header(header: str) -> Optional[str]:
"""Return a source file (relative path) that includes the given header,
or None if no good match.
LLVM doesn't list headers in compile_commands.json, so idt needs a
source TU to parse. With the `hasBody` / out-of-line distinction inside
idt, we can pick the conventional `Foo.cpp` partner without losing
coverage of `Foo.h`'s own symbols.
Resolution order:
1. Manual override in HEADER_SOURCE_OVERRIDES.
2. Direct mapping: llvm/include/llvm/Foo/Bar.h -> llvm/lib/Foo/Bar.cpp,
with INCLUDE_TO_LIB_SUBDIR applied (e.g. ADT -> Support).
3. Same-subdirectory grep: any .cpp under llvm/lib/Foo/ that
`#include`s the header. Constrained to the matching subdirectory
so we don't accidentally pick a target-specific source as a
generic fallback.
4. Bail (returns None; the header is silently skipped).
"""
# 1. Manual override for special cases.
if header in HEADER_SOURCE_OVERRIDES:
return HEADER_SOURCE_OVERRIDES[header]
if not header.startswith("llvm/include/llvm/"):
return None
# 2. Direct mapping.
sub = header[len("llvm/include/llvm/") :]
first_part, _, rest = sub.partition("/")
remap = INCLUDE_TO_LIB_SUBDIR.get(first_part)
sub_remapped = f"{remap}/{rest}" if remap and rest else sub
for ext in (".cpp", ".cc"):
candidate = Path("llvm/lib") / Path(sub_remapped).with_suffix(ext)
if candidate.exists():
return str(candidate)
# 3. Same-subdirectory grep fallback.
lib_subdir = f"llvm/lib/{INCLUDE_TO_LIB_SUBDIR.get(first_part, first_part)}"
if not Path(lib_subdir).is_dir():
return None
inc = header.removeprefix("llvm/include/")
try:
result = subprocess.run(
["git", "grep", "-l", f'#include "{inc}"', "--", lib_subdir],
capture_output=True,
text=True,
timeout=15,
)
except subprocess.TimeoutExpired:
return None
if result.returncode != 0 or not result.stdout:
return None
for line in result.stdout.splitlines():
if line.endswith((".cpp", ".cc")):
return line
# 4. Bail.
return None
class IdsCheckArgs:
start_rev: str = ""
end_rev: str = ""
changed_files: List[str] = []
idt_path: str = ""
compile_commands: str = ""
repo: str = ""
token: str = ""
issue_number: int = 0
verbose: bool = False
def __init__(self, args: argparse.Namespace) -> None:
self.start_rev = args.start_rev
self.end_rev = args.end_rev
self.changed_files = args.changed_files
self.idt_path = args.idt_path
self.compile_commands = args.compile_commands
self.repo = getattr(args, "repo", "")
self.token = getattr(args, "token", "")
self.issue_number = getattr(args, "issue_number", 0)
self.verbose = getattr(args, "verbose", False)
class IdsChecker:
"""Thin orchestrator around the idt binary."""
COMMENT_TAG = "<!--LLVM IDS CHECK COMMENT-->"
name = "ids-check"
friendly_name = "LLVM ABI annotation checker"
comment: dict = {}
@property
def comment_tag(self) -> str:
return self.COMMENT_TAG
@property
def instructions(self) -> str:
return (
"Build idt from compnerd/ids, then for each changed header:\n"
" idt -p build/ --main-file <matching-source.cpp> \\\n"
" --apply-fixits --inplace <header>"
)
def pr_comment_text_for_diff(self, diff: str) -> str:
return f"""
:warning: {self.friendly_name}, {self.name} found issues in your code. :warning:
<details>
<summary>
You can test this locally with the following command:
</summary>
``````````bash
{self.instructions}
``````````
</details>
<details>
<summary>
View the diff from {self.name} here.
</summary>
``````````diff
{diff}
``````````
</details>
"""
def update_pr(
self, comment_text: str, args: IdsCheckArgs, create_new: bool
) -> None:
import github
repo = github.Github(auth=github.Auth.Token(args.token)).get_repo(args.repo)
pr = repo.get_issue(args.issue_number).as_pull_request()
comment_text = self.comment_tag + "\n\n" + comment_text
existing_comment = None
for comment in pr.as_issue().get_comments():
if self.comment_tag in comment.body:
existing_comment = comment
break
if existing_comment:
self.comment = {"body": comment_text, "id": existing_comment.id}
elif create_new:
self.comment = {"body": comment_text}
def run_idt_for_category(
self,
category: str,
header_source_pairs: List[tuple],
args: IdsCheckArgs,
idt_path: str,
compile_commands: str,
) -> None:
"""Invoke idt once per header, using the matching source as the
flag donor via idt's `--main-file` option. idt looks the donor up
in compile_commands.json, swaps its input argument for the
positional header, and forces `-x c++-header` — so the header is
parsed standalone as the translation unit.
Per-header subprocess invocation also sidesteps idt's known
cross-TU state issues (DiagnosticsEngine instance lifetime in
batched runs).
"""
if not header_source_pairs:
return
if args.verbose:
print(
f"Running idt on {len(header_source_pairs)} {category} file(s)...",
file=sys.stderr,
)
def fwd(p: str) -> str:
# Forward slashes so paths survive cl::TokenizeGNUCommandLine.
return p.replace("\\", "/")
# Resolve `source` to the absolute path stored in
# compile_commands.json so idt's CompilationDatabase lookup is
# exact rather than relying on suffix-matching heuristics.
try:
with open(compile_commands, "r", encoding="utf-8") as f:
cdb_entries = json.load(f)
except Exception as e:
print(f" ERROR: failed to read {compile_commands}: {e}", file=sys.stderr)
return
cdb_paths = [fwd(e["file"]) for e in cdb_entries]
cdb_dir = os.path.dirname(compile_commands) or "."
for i, (header, source) in enumerate(header_source_pairs, start=1):
if args.verbose:
print(
f" [{i}/{len(header_source_pairs)}] {header} "
f"(flags from {source})",
file=sys.stderr,
)
source_norm = fwd(source)
donor_path = next((p for p in cdb_paths if p.endswith(source_norm)), None)
if donor_path is None:
print(
f" WARN: no compile_commands entry matching {source}; "
f"skipping {header}.",
file=sys.stderr,
)
continue
cmd = [
idt_path,
"-p",
fwd(cdb_dir),
f"--main-file={donor_path}",
f"--export-macro={EXPORT_MACRO[category]}",
f"--ignore={','.join(IGNORED_SYMBOLS)}",
"--apply-fixits",
"--inplace",
]
for extra in EXTRA_ARGS_COMMON:
cmd.append(f"--extra-arg={extra}")
cmd.append(fwd(header))
proc = subprocess.run(cmd)
if proc.returncode != 0 and proc.returncode != 1:
print(
f" WARN: idt exited with rc={proc.returncode} on "
f"{header}; continuing.",
file=sys.stderr,
)
def get_changed_files(self, args: IdsCheckArgs) -> List[str]:
"""Get list of changed files between revisions."""
if args.changed_files:
return args.changed_files
cmd = ["git", "diff", "--name-only", args.start_rev, args.end_rev]
if args.verbose:
print(f"Running: {' '.join(cmd)}", file=sys.stderr)
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
print("Error: Failed to get changed files", file=sys.stderr)
sys.stderr.write(proc.stderr.decode("utf-8"))
return []
files = proc.stdout.decode("utf-8").strip().split("\n")
return [f for f in files if f]
def check_for_diff(self) -> Optional[str]:
"""Check if there are any uncommitted changes after running idt."""
cmd = ["git", "diff"]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
diff = proc.stdout.decode("utf-8")
if diff:
return diff
return None
def run(self, args: IdsCheckArgs) -> int:
"""Main entry point for running ids check."""
# Resolve idt path: prefer command line arg, then env var
idt_path = args.idt_path or os.environ.get("IDT_PATH")
if not idt_path:
print(
"Error: idt path not specified. Use --idt-path argument or set IDT_PATH environment variable",
file=sys.stderr,
)
return 1
if not os.path.exists(idt_path):
print(f"Error: idt tool not found at {idt_path}", file=sys.stderr)
return 1
# Resolve compile_commands path: prefer command line arg, then env var
compile_commands = args.compile_commands or os.environ.get(
"COMPILE_COMMANDS_PATH"
)
if not compile_commands:
print(
"Error: compile_commands.json path not specified. Use --compile-commands argument or set COMPILE_COMMANDS_PATH environment variable",
file=sys.stderr,
)
return 1
if not os.path.exists(compile_commands):
print(
f"Error: compile_commands.json not found at {compile_commands}",
file=sys.stderr,
)
return 1
# Get changed files
changed_files = self.get_changed_files(args)
if not changed_files:
if args.verbose:
print("No files changed, skipping ids check", file=sys.stderr)
return 0
# Filter to relevant headers and group by category.
per_category = defaultdict(list)
for path in changed_files:
if not path.endswith(".h"):
continue
if not path.startswith(("llvm/include/llvm/", "llvm/include/llvm-c/")):
continue
if any(path.startswith(p) for p in SKIP_HEADERS):
continue
category = categorize_header(path)
if category is None:
continue
source = find_source_for_header(path)
if source is None:
if args.verbose:
print(f" Skipping {path}: no matching source", file=sys.stderr)
continue
per_category[category].append((path, source))
if not per_category:
if args.verbose:
print(
"No relevant header files changed, skipping ids check",
file=sys.stderr,
)
return 0
for category, pairs in per_category.items():
self.run_idt_for_category(category, pairs, args, idt_path, compile_commands)
# Check for differences
diff = self.check_for_diff()
should_update_gh = args.token is not None and args.repo is not None
if diff:
if should_update_gh:
comment_text = self.pr_comment_text_for_diff(diff)
self.update_pr(comment_text, args, create_new=True)
else:
print(
"\nError: idt found missing LLVM_ABI annotations", file=sys.stderr
)
print(
"Apply the following diff to fix the LLVM_ABI annotations:\n",
file=sys.stderr,
)
print(diff)
return 1
else:
if should_update_gh:
comment_text = (
":white_check_mark: With the latest revision "
f"this PR passed the {self.friendly_name}."
)
self.update_pr(comment_text, args, create_new=False)
if args.verbose:
print("All files pass ids check", file=sys.stderr)
return 0
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Check LLVM ABI annotations in header files"
)
parser.add_argument("--token", type=str, help="GitHub authentication token")
parser.add_argument(
"--repo",
type=str,
default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"),
help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)",
)
parser.add_argument("--issue-number", type=int, help="GitHub issue/PR number")
parser.add_argument(
"--start-rev",
type=str,
required=True,
help="Compute changes from this revision",
)
parser.add_argument(
"--end-rev",
type=str,
required=True,
help="Compute changes to this revision",
)
parser.add_argument(
"--changed-files",
type=str,
help="Comma-separated list of changed files, or `@<path>` to read "
"the list from a response file (one path per line or "
"comma-separated). The `@`-prefix follows the standard "
"compiler/linker convention and is useful on Windows where the "
"inline comma-separated form can hit command-line length limits.",
)
parser.add_argument(
"--idt-path",
type=str,
help="Path to the idt executable (can also be set via IDT_PATH environment variable)",
)
parser.add_argument(
"--compile-commands",
type=str,
help="Path to compile_commands.json (can also be set via COMPILE_COMMANDS_PATH environment variable)",
)
parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
parsed_args = parser.parse_args()
raw = parsed_args.changed_files or ""
if raw.startswith("@"):
# Response file: read its contents and treat the inline value as if
# it had been spelled inline.
with open(raw[1:], "r", encoding="utf-8") as f:
raw = f.read()
# Accept both comma-separated and newline-separated entries so the
# response-file variant is friendly to write line-by-line.
parsed_args.changed_files = [
token.strip()
for line in raw.splitlines()
for token in line.split(",")
if token.strip()
]
args = IdsCheckArgs(parsed_args)
checker = IdsChecker()
exit_code = checker.run(args)
if checker.comment:
with open("comments", "w") as f:
import json
json.dump([checker.comment], f)
sys.exit(exit_code)