[llvm] Reduce false positives in ids-check workflow (PR #194878)
Fabrice de Gans via llvm-commits
llvm-commits at lists.llvm.org
Wed Apr 29 08:55:29 PDT 2026
https://github.com/Steelskin updated https://github.com/llvm/llvm-project/pull/194878
>From 86e05c090e448e80c849e869ae0a741f2636528b Mon Sep 17 00:00:00 2001
From: Fabrice de Gans <fabrice at thebrowser.company>
Date: Wed, 29 Apr 2026 16:28:25 +0200
Subject: [PATCH] Reduce false positives in ids-check workflow
The ids-check workflow is meant to signal when LLVM APIs are missing
from a header modified in a PR. However, there were a few issues with
the initial implementation:
* LLVM_ABI annotations were incorrectly suggested for headers that
should not have them.
* Unmodified headers were also signaled as missing LLVM_ABI annotations.
These changes attempt to remedy the situation by excluding whole
categories of headers for which the workflow does not work properly and
improving the way headers are parsed.
These changes do not re-enable the workflow yet. This will be done
after all of the missing LLVM_ABI annotations have been added, using the
changes to the script.
---
.github/workflows/ids-check.yml | 83 ++-
llvm/include/llvm/Support/CommandLine.h | 4 +-
llvm/utils/git/ids-check-helper.py | 492 +++++++------
llvm/utils/idt/CMakeLists.txt | 45 ++
llvm/utils/idt/LICENSE.TXT | 29 +
llvm/utils/idt/README.md | 35 +
llvm/utils/idt/idt.cc | 883 ++++++++++++++++++++++++
7 files changed, 1325 insertions(+), 246 deletions(-)
create mode 100644 llvm/utils/idt/CMakeLists.txt
create mode 100644 llvm/utils/idt/LICENSE.TXT
create mode 100644 llvm/utils/idt/README.md
create mode 100644 llvm/utils/idt/idt.cc
diff --git a/.github/workflows/ids-check.yml b/.github/workflows/ids-check.yml
index f4c3c8a8453bf..20046147cc686 100644
--- a/.github/workflows/ids-check.yml
+++ b/.github/workflows/ids-check.yml
@@ -3,8 +3,8 @@ name: "Check LLVM ABI annotations"
# TODO(https://github.com/llvm/llvm-project/issues/109483): Re-enable on pull
# requests once the workflow is less diruptive.
on:
- # pull_request:
- workflow_dispatch:
+ pull_request:
+ # workflow_dispatch:
permissions:
contents: read
@@ -18,16 +18,9 @@ jobs:
if: github.repository_owner == 'llvm'
name: Check LLVM_ABI annotations with ids
runs-on: ubuntu-24.04
- timeout-minutes: 10
+ timeout-minutes: 30
steps:
- - uses: actions/checkout at de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
- repository: compnerd/ids
- path: ${{ github.workspace }}/ids
- ref: b3bf35dd13d7ff244a6a6d106fe58d0eedb5743e # main
-
- uses: actions/checkout at de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -46,7 +39,16 @@ jobs:
- name: Install dependencies
run: |
- sudo apt install -y clang-19 ninja-build libclang-19-dev
+ # Pull a recent clang from LLVM's apt repo so idt's parser stays in
+ # sync with the C++ language features used by current LLVM source.
+ sudo install -d -m 0755 /etc/apt/keyrings
+ wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \
+ | sudo gpg --dearmor -o /etc/apt/keyrings/llvm.gpg
+ echo "deb [signed-by=/etc/apt/keyrings/llvm.gpg] http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" \
+ | sudo tee /etc/apt/sources.list.d/llvm.list
+ sudo apt update
+ # oprofile provides opagent.h, used by llvm/ExecutionEngine/OProfileWrapper.h
+ sudo apt install -y clang-22 lld-22 ninja-build libclang-22-dev oprofile
pip install --require-hashes -r ${{ github.workspace }}/llvm-project/llvm/utils/git/requirements.txt
- name: Configure and build minimal LLVM for use by ids
@@ -54,33 +56,54 @@ jobs:
cmake -B ${{ github.workspace }}/llvm-project/build/ \
-S ${{ github.workspace }}/llvm-project/llvm/ \
-D CMAKE_BUILD_TYPE=Release \
- -D CMAKE_C_COMPILER=clang \
- -D CMAKE_CXX_COMPILER=clang++ \
+ -D CMAKE_C_COMPILER=clang-22 \
+ -D CMAKE_CXX_COMPILER=clang++-22 \
-D LLVM_ENABLE_PROJECTS=clang \
-D LLVM_TARGETS_TO_BUILD="host" \
+ -D LLVM_INCLUDE_TESTS=OFF \
+ -D LLVM_INCLUDE_BENCHMARKS=OFF \
-D CMAKE_EXPORT_COMPILE_COMMANDS=ON \
+ -D CMAKE_DISABLE_PRECOMPILE_HEADERS=ON \
-G Ninja
- cd ${{ github.workspace }}/llvm-project/build/
- ninja -t targets all | grep "CommonTableGen: phony$" | grep -v "/" | sed 's/:.*//'
- - name: Configure ids
+ # Build the generated-header prerequisites that idt needs to parse
+ # LLVM source.
+ BUILD_DIR=${{ github.workspace }}/llvm-project/build
+ ninja -C "$BUILD_DIR" \
+ llvm_vcsrevision_h \
+ intrinsics_gen omp_gen acc_gen analysis_gen target_parser_gen vt_gen \
+ DllOptionsTableGen LibOptionsTableGen \
+ clang-tablegen-targets \
+ lib/ExecutionEngine/JITLink/COFFOptions.inc
+
+ # Build per-target tablegen output for every enabled target.
+ # Each enabled target exposes a top-level `<Target>CommonTableGen`
+ # phony rule (e.g. `X86CommonTableGen`); discover them from the
+ # ninja graph.
+ TABLEGEN_TARGETS=$(ninja -C "$BUILD_DIR" -t targets all \
+ | awk -F: '
+ $2 ~ /phony/ && # phony rules only
+ $1 !~ /\// && # top-level (skip nested paths)
+ $1 ~ /CommonTableGen$/ { # per-target tablegen aggregator
+ print $1
+ }')
+ ninja -C "$BUILD_DIR" $TABLEGEN_TARGETS
+
+ - name: Configure and build idt
run: |
- cmake -B ${{ github.workspace }}/ids/build/ \
- -S ${{ github.workspace }}/ids/ \
+ # idt lives in-tree under llvm/utils/idt as a vendored standalone
+ # CMake project. It links against the apt-installed libclang-22-dev,
+ # not the just-configured LLVM build, so the build is small.
+ IDT_SRC=${{ github.workspace }}/llvm-project/llvm/utils/idt
+ cmake -B "$IDT_SRC/build" -S "$IDT_SRC" \
-D CMAKE_BUILD_TYPE=Release \
- -D CMAKE_C_COMPILER=clang \
- -D CMAKE_CXX_COMPILER=clang++ \
+ -D CMAKE_C_COMPILER=clang-22 \
+ -D CMAKE_CXX_COMPILER=clang++-22 \
-D CMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld \
- -D LLVM_DIR=/usr/lib/llvm-19/lib/cmake/llvm/ \
- -D Clang_DIR=/usr/lib/llvm-19/lib/cmake/clang/ \
- -D FILECHECK_EXECUTABLE=$(which FileCheck-19) \
- -D LIT_EXECUTABLE=$(which lit) \
+ -D LLVM_DIR=/usr/lib/llvm-22/lib/cmake/llvm/ \
+ -D Clang_DIR=/usr/lib/llvm-22/lib/cmake/clang/ \
-G Ninja
-
- # TODO: Use an image with a prebuilt idt.
- - name: Build ids
- run: |
- ninja -C ${{ github.workspace }}/ids/build/ all
+ ninja -C "$IDT_SRC/build"
- name: Run ids check
env:
@@ -92,7 +115,7 @@ jobs:
python llvm/utils/git/ids-check-helper.py \
--token ${{ secrets.GITHUB_TOKEN }} \
--issue-number $GITHUB_PR_NUMBER \
- --idt-path ${{ github.workspace }}/ids/build/bin/idt \
+ --idt-path ${{ github.workspace }}/llvm-project/llvm/utils/idt/build/bin/idt \
--compile-commands ${{ github.workspace }}/llvm-project/build/compile_commands.json \
--start-rev HEAD~1 \
--end-rev HEAD \
diff --git a/llvm/include/llvm/Support/CommandLine.h b/llvm/include/llvm/Support/CommandLine.h
index be754f3c159ca..911411e0a457d 100644
--- a/llvm/include/llvm/Support/CommandLine.h
+++ b/llvm/include/llvm/Support/CommandLine.h
@@ -206,8 +206,8 @@ class SubCommand {
StringRef Description;
protected:
- LLVM_ABI void registerSubCommand();
- LLVM_ABI void unregisterSubCommand();
+ void registerSubCommand();
+ void unregisterSubCommand();
public:
SubCommand(StringRef Name, StringRef Description = "")
diff --git a/llvm/utils/git/ids-check-helper.py b/llvm/utils/git/ids-check-helper.py
index 1f0b983fb2261..844297482c2b0 100755
--- a/llvm/utils/git/ids-check-helper.py
+++ b/llvm/utils/git/ids-check-helper.py
@@ -8,28 +8,204 @@
import os
import subprocess
import sys
+from collections import defaultdict
+from pathlib import Path
from typing import List, Optional
"""
-This script is run by GitHub actions to ensure that the code in PRs properly
-labels LLVM APIs with `LLVM_ABI` so as not to break the LLVM DLL build. It can
-also be installed as a pre-commit git hook to check ABI annotations before
-submitting. The canonical source of this script is in the LLVM source tree
-under llvm/utils/git.
+Run idt on the headers changed in a PR and (optionally) post the resulting
+LLVM_ABI annotation diff as a PR comment.
-This script uses the idt (Interface Diff Tool) to check for missing LLVM_ABI,
-LLVM_C_ABI, and DEMANGLE_ABI annotations in header files.
+The heavy lifting (per-category clang flags, system-header-prefix isolation,
+output filtering, the `hasBody` / out-of-line distinction) lives in
+`llvm/utils/idt/idt.cc`. 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.
-You can install this script as a git hook by symlinking it to the .git/hooks
-directory:
+Usage as a git pre-commit hook:
-ln -s $(pwd)/llvm/utils/git/ids-check-helper.py .git/hooks/pre-commit
+ ln -s $(pwd)/llvm/utils/git/ids-check-helper.py .git/hooks/pre-commit
-You can control the exact path to idt and compile_commands.json with the
-following environment variables: $IDT_PATH and $COMPILE_COMMANDS_PATH.
+Environment variables `IDT_PATH` and `COMPILE_COMMANDS_PATH` may be used in
+place of the matching command-line arguments.
"""
+# 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 = [
+ # PDB DIA requires Windows ATL (atlbase.h), unbuildable on Linux runners.
+ # The direct mapping picks lib/DebugInfo/PDB/DIA/*.cpp which can't parse.
+ "llvm/include/llvm/DebugInfo/PDB/DIA/",
+ # 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", # only lli.cpp
+ "llvm/include/llvm/Support/DebugLog.h", # only lib/Target/RISCV/
+ "llvm/include/llvm/Support/TargetSelect.h", # only target-registration sources
+]
+
+
+# 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/LLJITUtilsCBindings.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 must match `kLlvmCategories` in `llvm/utils/idt/idt.cc`. We
+# duplicate the table here only to batch headers per category before
+# invoking idt; idt itself derives the export macro / include header / system
+# prefixes from the same path-fragment match. Order matters: more specific
+# fragments first.
+CATEGORIES = [
+ ("Demangle", "llvm/Demangle/"),
+ ("LLVM-C", "llvm-c/"),
+ ("LLVM", "llvm/"),
+]
+
+
+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 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).
+ """
+ if header in HEADER_SOURCE_OVERRIDES:
+ return HEADER_SOURCE_OVERRIDES[header]
+
+ if not header.startswith("llvm/include/llvm/"):
+ return None
+
+ 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)
+
+ 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
+ return None
+
+
class IdsCheckArgs:
start_rev: str = ""
end_rev: str = ""
@@ -54,27 +230,24 @@ def __init__(self, args: argparse.Namespace) -> None:
class IdsChecker:
- """
- Checker for LLVM ABI annotations using the idt tool.
- """
+ """Thin orchestrator around the idt binary."""
COMMENT_TAG = "<!--LLVM IDS CHECK COMMENT-->"
name = "ids-check"
friendly_name = "LLVM ABI annotation checker"
comment: dict = {}
- # Macro definition used for all export macros
- MACRO_DEFINITION = '__attribute__((visibility("default")))'
-
@property
def comment_tag(self) -> str:
return self.COMMENT_TAG
@property
def instructions(self) -> str:
- # Provide basic usage instructions
- return f"""git diff origin/main HEAD -- 'llvm/include/llvm/**/*.h' 'llvm/include/llvm-c/**/*.h' 'llvm/include/llvm/Demangle/**/*.h'
-Then run idt on the changed files with appropriate --export-macro and --include-header flags."""
+ return (
+ "Build idt under llvm/utils/idt/, then for each changed header:\n"
+ " idt --header <header> -p build/compile_commands.json \\\n"
+ " --apply-fixits --inplace <matching-source.cpp>"
+ )
def pr_comment_text_for_diff(self, diff: str) -> str:
return f"""
@@ -89,12 +262,6 @@ def pr_comment_text_for_diff(self, diff: str) -> str:
{self.instructions}
``````````
-:warning:
-The reproduction instructions above might return results for more than one PR
-in a stack if you are using a stacked PR workflow. You can limit the results by
-changing `origin/main` to the base branch/commit you want to compare against.
-:warning:
-
</details>
<details>
@@ -130,218 +297,135 @@ def update_pr(
elif create_new:
self.comment = {"body": comment_text}
- # Define the file categories and their corresponding configurations
- FILE_CATEGORIES = [
- {
- "name": "LLVM headers",
- "patterns": ["llvm/include/llvm/**/*.h"],
- "excludes": [
- "llvm/include/llvm/Debuginfod/",
- "llvm/include/llvm/Demangle/",
- ],
- "export_macro": "LLVM_ABI",
- "include_header": "llvm/Support/Compiler.h",
- },
- {
- "name": "LLVM-C headers",
- "patterns": ["llvm/include/llvm-c/**/*.h"],
- "excludes": [],
- "export_macro": "LLVM_C_ABI",
- "include_header": "llvm-c/Visibility.h",
- },
- {
- "name": "LLVM Demangle headers",
- "patterns": ["llvm/include/llvm/Demangle/**/*.h"],
- "excludes": [],
- "export_macro": "DEMANGLE_ABI",
- "include_header": "llvm/Demangle/Visibility.h",
- },
- ]
-
- def filter_files_for_category(
- self, changed_files: List[str], category: dict
- ) -> List[str]:
- """Filter changed files based on category patterns and excludes."""
- filtered = []
- for path in changed_files:
- # Check if file matches any pattern
- matches_pattern = False
- for pattern in category["patterns"]:
- # Simple pattern matching for **/*.h style patterns
- pattern_prefix = pattern.replace("**/*.h", "")
- if path.startswith(pattern_prefix) and path.endswith(".h"):
- matches_pattern = True
- break
-
- if not matches_pattern:
- continue
-
- # Check if file should be excluded
- excluded = False
- for exclude in category["excludes"]:
- if path.startswith(exclude):
- excluded = True
- break
-
- if not excluded:
- filtered.append(path)
-
- return filtered
-
- def run_idt_on_files(
- self,
- files: List[str],
- category: dict,
- args: IdsCheckArgs,
- idt_path: str,
- compile_commands: str,
- ) -> bool:
- """Run idt tool on the given files with category-specific configuration."""
- if not files:
- return True
+ 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 for all headers in a category. idt is told the full
+ list of headers via repeated --header, and the full list of unique
+ source TUs as positional arguments. idt scopes its diagnostics to the
+ listed headers and applies fix-its in place."""
+ if not header_source_pairs:
+ return
+
+ sources = sorted({s for _, s in header_source_pairs})
+ cmd = [idt_path, "-p", compile_commands, "--apply-fixits", "--inplace"]
+ for h, _ in header_source_pairs:
+ cmd += [f"--header={h}"]
+ cmd += sources
if args.verbose:
print(
- f"Running idt on {len(files)} {category['name']} file(s)...",
+ f"Running idt on {len(header_source_pairs)} {category} header(s) "
+ f"via {len(sources)} source TU(s)...",
file=sys.stderr,
)
- for file in files:
- cmd = [
- idt_path,
- "-p",
- compile_commands,
- "--apply-fixits",
- "--inplace",
- f"--export-macro={category['export_macro']}",
- f"--include-header={category['include_header']}",
- f"--extra-arg=-D{category['export_macro']}={self.MACRO_DEFINITION}",
- "--extra-arg=-Wno-macro-redefined",
- file,
- ]
-
- if args.verbose:
- print(f"Running: {' '.join(cmd)}", file=sys.stderr)
-
- subprocess.run(cmd)
-
- return True
+ subprocess.run(cmd)
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)
-
+ proc = subprocess.run(["git", "diff"], stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
diff = proc.stdout.decode("utf-8")
- if diff:
- return diff
- return None
+ return diff if diff else 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,
- )
+ print("Error: idt path not specified. Use --idt-path or set IDT_PATH.",
+ 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,
- )
+ print("Error: --compile-commands not specified.", 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,
- )
+ 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
- # Process each category
- any_processed = False
- for category in self.FILE_CATEGORIES:
- filtered_files = self.filter_files_for_category(changed_files, category)
- if filtered_files:
- any_processed = True
- if not self.run_idt_on_files(
- filtered_files, category, args, idt_path, compile_commands
- ):
- return 1
-
- if not any_processed:
+ # 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,
- )
+ print("No relevant header files changed, skipping ids check",
+ file=sys.stderr)
return 0
- # Check for differences
+ for category, pairs in per_category.items():
+ self.run_idt_for_category(
+ category, pairs, args, idt_path, compile_commands
+ )
+
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)
+ self.update_pr(self.pr_comment_text_for_diff(diff),
+ 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("\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 should_update_gh:
+ self.update_pr(
+ ":white_check_mark: With the latest revision "
+ f"this PR passed the {self.friendly_name}.",
+ args, create_new=False,
+ )
+ if args.verbose:
+ print("All files pass ids check", file=sys.stderr)
+ return 0
if __name__ == "__main__":
@@ -353,43 +437,24 @@ def run(self, args: IdsCheckArgs) -> int:
"--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)",
+ help="GitHub repository <owner>/<repo>",
)
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 files that have been changed",
- )
- 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", default=True, help="Enable verbose output"
- )
+ 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 files that have been changed")
+ parser.add_argument("--idt-path", type=str,
+ help="Path to the idt executable (or set IDT_PATH)")
+ parser.add_argument("--compile-commands", type=str,
+ help="Path to compile_commands.json (or set COMPILE_COMMANDS_PATH)")
+ parser.add_argument("--verbose", action="store_true", default=True,
+ help="Enable verbose output")
parsed_args = parser.parse_args()
- # Parse changed files if provided
if parsed_args.changed_files:
parsed_args.changed_files = [
f.strip() for f in parsed_args.changed_files.split(",") if f.strip()
@@ -404,7 +469,6 @@ def run(self, args: IdsCheckArgs) -> int:
if checker.comment:
with open("comments", "w") as f:
import json
-
json.dump([checker.comment], f)
sys.exit(exit_code)
diff --git a/llvm/utils/idt/CMakeLists.txt b/llvm/utils/idt/CMakeLists.txt
new file mode 100644
index 0000000000000..5e9ab6bb3e7cd
--- /dev/null
+++ b/llvm/utils/idt/CMakeLists.txt
@@ -0,0 +1,45 @@
+cmake_minimum_required(VERSION 3.20)
+project(idt LANGUAGES C CXX)
+
+# This is a vendored copy of the Interface Definition Scanner (idt) from
+# https://github.com/compnerd/ids. It is built as a standalone CMake project
+# against a pre-installed LLVM/Clang (LLVM_DIR / Clang_DIR), not as part of
+# the main LLVM build. See README.md for context.
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED YES)
+set(CMAKE_CXX_EXTENSIONS NO)
+
+set(CMAKE_CXX_VISIBILITY_PRESET hidden)
+set(CMAKE_CXX_VISIBILITY_INLINES_HIDDEN yes)
+
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+
+find_package(LLVM REQUIRED)
+message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
+message(STATUS "Using LLVMConfig.cmake in ${LLVM_DIR}")
+
+find_package(Clang REQUIRED)
+message(STATUS "Found Clang ${Clang_PACKAGE_VERSION}")
+message(STATUS "Using ClangConfig.cmake in ${Clang_DIR}")
+
+add_executable(idt idt.cc)
+
+target_compile_definitions(idt PRIVATE
+ ${LLVM_DEFINITIONS})
+
+target_compile_options(idt PRIVATE
+ $<$<CXX_COMPILER_ID:MSVC>:/EHsc- /GR->
+ $<$<CXX_COMPILER_ID:AppleClang>:-fno-exceptions -fno-rtti>
+ $<$<CXX_COMPILER_ID:Clang>:-fno-exceptions -fno-rtti>
+ $<$<CXX_COMPILER_ID:GNU>:-fno-exceptions -fno-rtti>)
+
+target_include_directories(idt PRIVATE
+ ${LLVM_INCLUDE_DIRS}
+ ${CLANG_INCLUDE_DIRS})
+
+target_link_libraries(idt PRIVATE
+ clangRewriteFrontend
+ clangTooling)
diff --git a/llvm/utils/idt/LICENSE.TXT b/llvm/utils/idt/LICENSE.TXT
new file mode 100644
index 0000000000000..b4604761aefac
--- /dev/null
+++ b/llvm/utils/idt/LICENSE.TXT
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2021, Saleem Abdulrasool
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/llvm/utils/idt/README.md b/llvm/utils/idt/README.md
new file mode 100644
index 0000000000000..2010dfdbd2c26
--- /dev/null
+++ b/llvm/utils/idt/README.md
@@ -0,0 +1,35 @@
+# Interface Definition Scanner (idt)
+
+This is a vendored copy of [`compnerd/ids`](https://github.com/compnerd/ids),
+a libclang-based tool for identifying and annotating the public interface of a
+C++ project (e.g. adding `LLVM_ABI` markers to declarations that need them for
+the `LLVM_BUILD_LLVM_DYLIB` build).
+
+It is used by `.github/workflows/ids-check.yml` together with
+`llvm/utils/git/ids-check-helper.py` to flag missing ABI annotations on PRs.
+
+## Why vendored?
+
+idt's heuristics for "what needs exporting" were designed for typical libraries
+and don't all hold for LLVM (e.g. the workflow needs to distinguish out-of-line
+`.cpp` definitions from inline-in-header definitions). Vendoring lets us patch
+the tool to fit LLVM's needs without round-tripping through the upstream repo.
+
+## Build
+
+This is a **standalone** CMake project; it is not built as part of the main
+LLVM build. It links against a pre-installed LLVM/Clang (typically the
+`libclang-*-dev` package on the CI runner), which keeps build time small
+(seconds, not the half-hour required to build clang from source).
+
+```sh
+cmake -B build -S . -G Ninja \
+ -D LLVM_DIR=/usr/lib/llvm-22/lib/cmake/llvm/ \
+ -D Clang_DIR=/usr/lib/llvm-22/lib/cmake/clang/
+ninja -C build
+```
+
+## License
+
+idt is BSD-3-Clause licensed (see `LICENSE.TXT`). LLVM is Apache-2.0 with LLVM
+exceptions; the BSD-3-Clause notice is preserved on the vendored source files.
diff --git a/llvm/utils/idt/idt.cc b/llvm/utils/idt/idt.cc
new file mode 100644
index 0000000000000..8b6561fe59433
--- /dev/null
+++ b/llvm/utils/idt/idt.cc
@@ -0,0 +1,883 @@
+// Copyright (c) 2021 Saleem Abdulrasool. All Rights Reserved.
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/Basic/Diagnostic.h"
+#include "clang/Frontend/CompilerInstance.h"
+#include "clang/Lex/PPCallbacks.h"
+#include "clang/Lex/Preprocessor.h"
+#include "clang/Rewrite/Frontend/FixItRewriter.h"
+#include "clang/Tooling/ArgumentsAdjusters.h"
+#include "clang/Tooling/CommonOptionsParser.h"
+#include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Path.h"
+
+#include <algorithm>
+#include <array>
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+#include <optional>
+#include <string>
+#include <tuple>
+#include <unordered_map>
+#include <vector>
+
+namespace idt {
+llvm::cl::OptionCategory category{"interface definition scanner options"};
+}
+
+namespace {
+// TODO(compnerd) make this configurable via a configuration file or commandline
+const std::set<std::string> kIgnoredBuiltins{
+ "_BitScanForward",
+ "_BitScanForward64",
+ "_BitScanReverse",
+ "_BitScanReverse64",
+ "__builtin_strlen",
+};
+
+// LLVM headers being checked. idt derives the export macro, the "needs
+// include" header, and the cross-category system-header-prefix flags from
+// these paths. May be specified multiple times to batch multiple headers
+// into one ClangTool invocation; all listed headers must belong to the
+// same LLVM category. Only diagnostics whose source location lies inside
+// one of the listed headers are reported.
+llvm::cl::list<std::string>
+headers("header",
+ llvm::cl::desc("LLVM header to check (e.g. llvm/Support/Foo.h). "
+ "Repeat to batch multiple headers; all must share "
+ "the same category."),
+ llvm::cl::value_desc("path"), llvm::cl::OneOrMore,
+ llvm::cl::cat(idt::category));
+
+llvm::cl::opt<bool>
+apply_fixits("apply-fixits", llvm::cl::init(false),
+ llvm::cl::desc("Apply suggested changes to decorate interfaces"),
+ llvm::cl::cat(idt::category));
+
+llvm::cl::opt<bool>
+inplace("inplace", llvm::cl::init(false),
+ llvm::cl::desc("Apply suggested changes in-place"),
+ llvm::cl::cat(idt::category));
+
+// Resolved category for `--header`. Set by main() once the option is parsed
+// and consumed by the visitor + diagnostic emitters.
+const struct llvm_category *active_category = nullptr;
+
+// Resolved per-category strings, populated from `active_category`. These used
+// to be standalone command-line options on idt; they are now derived.
+std::string export_macro;
+std::string include_header;
+
+template <typename Key, typename Compare, typename Allocator>
+bool contains(const std::set<Key, Compare, Allocator>& set, const Key& key) {
+ return set.find(key) != set.end();
+}
+
+// LLVM-specific category table. Each entry maps a distinguishing path
+// fragment to the export-macro / include-header / system-header-prefix
+// configuration the project uses for that category of headers.
+//
+// `path_match` is matched as a substring against the --header argument so
+// that both repo-relative paths (`llvm/include/llvm-c/Core.h`) and
+// include-style paths (`llvm-c/Core.h`) work.
+//
+// Order matters: more specific fragments must come before less specific
+// ones (e.g. `llvm/Demangle/` before the catch-all LLVM entry).
+struct llvm_category {
+ llvm::StringRef path_match;
+ llvm::StringRef export_macro;
+ llvm::StringRef include_header;
+ llvm::ArrayRef<llvm::StringRef> system_header_prefixes;
+ llvm::ArrayRef<llvm::StringRef> no_system_header_prefixes;
+};
+
+// Headers in *other* categories are marked as system so clang's
+// `isInSystemHeader` filter excludes them. The `no_system_header_prefixes`
+// entries unmask sub-paths inside a marked region (e.g. Demangle's "llvm/"
+// is system, but "llvm/Demangle/" inside it is not).
+constexpr llvm::StringRef kLlvmSystemPrefixes[] = {
+ "llvm-c/", "llvm/Demangle/", "llvm/Debuginfod/"};
+constexpr llvm::StringRef kLlvmCSystemPrefixes[] = {"llvm/"};
+constexpr llvm::StringRef kDemangleSystemPrefixes[] = {"llvm/", "llvm-c/"};
+constexpr llvm::StringRef kDemangleNoSystemPrefixes[] = {"llvm/Demangle/"};
+
+const llvm_category kLlvmCategories[] = {
+ {"llvm/Demangle/", "DEMANGLE_ABI", "llvm/Demangle/Visibility.h",
+ kDemangleSystemPrefixes, kDemangleNoSystemPrefixes},
+ {"llvm-c/", "LLVM_C_ABI", "llvm-c/Visibility.h", kLlvmCSystemPrefixes, {}},
+ {"llvm/", "LLVM_ABI", "llvm/Support/Compiler.h", kLlvmSystemPrefixes, {}},
+};
+
+// Match `path` against the category table. Returns nullptr if no category
+// applies (e.g. a header outside the LLVM namespace).
+const llvm_category *find_llvm_category(llvm::StringRef path) {
+ for (const auto &cat : kLlvmCategories)
+ if (path.contains(cat.path_match))
+ return &cat;
+ return nullptr;
+}
+
+}
+
+namespace idt {
+struct PPCallbacks : clang::PPCallbacks {
+ // Describes the source location of an #include statement and the name of the
+ // file being included.
+ using IncludeLocation = std::tuple<std::string, clang::SourceLocation>;
+
+ // Maps the name of a source file to list of include statements its contains
+ // in the order they are discovered int he file.
+ using FileIncludes =
+ std::unordered_map<std::string, std::vector<IncludeLocation>>;
+
+ PPCallbacks(clang::SourceManager &source_manager, FileIncludes &file_includes)
+ : source_manager_(source_manager), file_includes_(file_includes) {}
+
+ void
+ InclusionDirective(clang::SourceLocation HashLoc,
+ const clang::Token &IncludeTok, clang::StringRef FileName,
+ bool IsAngled, clang::CharSourceRange FilenameRange,
+ clang::OptionalFileEntryRef File,
+ clang::StringRef SearchPath, clang::StringRef RelativePath,
+ const clang::Module *SuggestedModule, bool ModuleImported,
+ clang::SrcMgr::CharacteristicKind FileType) override {
+ // Only track #include statements not #import statements.
+ if (ModuleImported)
+ return;
+
+ // Track the name and location of each include in the order discovered.
+ clang::SourceLocation SLoc = source_manager_.getSpellingLoc(HashLoc);
+
+ // Get the name of the file that contains the #include statement. This
+ // string is distinct from the FileName function parameter, which is the
+ // name of the include target (e.g. #include <FileName>).
+ std::string containingFileName = source_manager_.getFilename(SLoc).str();
+
+ // Only add the include to the list if it isn't already present.
+ auto &includes = file_includes_[containingFileName];
+ if (std::none_of(includes.begin(), includes.end(),
+ [&FileName](const IncludeLocation &include) {
+ return std::get<0>(include) == FileName.str();
+ }))
+ includes.emplace_back(FileName.str(), SLoc);
+ }
+
+private:
+ clang::SourceManager &source_manager_;
+ FileIncludes &file_includes_;
+};
+
+// Track a set of clang::Decl declarations by unique ID.
+class DeclSet {
+ llvm::SmallPtrSet<std::uintptr_t, 32> decls_;
+
+ // Use pointer identity of the canonical declaration object as a unique ID.
+ template <typename Decl_>
+ std::uintptr_t decl_id(const Decl_ *D) const {
+ return reinterpret_cast<std::uintptr_t>(D->getCanonicalDecl());
+ }
+
+public:
+ template <typename Decl_>
+ inline void insert(const Decl_ *D) {
+ decls_.insert(decl_id(D));
+ }
+
+ template <typename Decl_>
+ inline bool contains(const Decl_ *D) const {
+ return decls_.find(decl_id(D)) != decls_.end();
+ }
+};
+
+class visitor : public clang::RecursiveASTVisitor<visitor> {
+ clang::ASTContext &context_;
+ clang::SourceManager &source_manager_;
+ std::optional<unsigned> id_unexported_;
+ std::optional<unsigned> id_improper_;
+ std::optional<unsigned> id_exported_;
+ PPCallbacks::FileIncludes &file_includes_;
+
+ // Accumulates the set of declarations that have been marked for export by
+ // this visitor.
+ DeclSet exported_decls_;
+
+ void add_missing_include(clang::SourceLocation location) {
+ if (include_header.empty())
+ return;
+
+ clang::DiagnosticsEngine &diagnostics_engine = context_.getDiagnostics();
+
+ static unsigned kID = diagnostics_engine.getCustomDiagID(
+ clang::DiagnosticsEngine::Remark, "missing include statement %0");
+
+ clang::SourceLocation spellingLoc =
+ source_manager_.getSpellingLoc(location);
+ const std::string fileName = source_manager_.getFilename(spellingLoc).str();
+ auto &includes = file_includes_[fileName];
+
+ // TODO: if the modified file contains no existing include directives, we
+ // cannot currently determine where to insert the required include.
+ if (includes.empty())
+ return;
+
+ // Determine if the header is already included.
+ if (std::any_of(includes.begin(), includes.end(),
+ [](const PPCallbacks::IncludeLocation &include) {
+ return std::get<0>(include) == include_header;
+ }))
+ return;
+
+ // Insert the new include at the start of the existing include list. Rely
+ // on clang-format to properly sort the include statements in alphabetical
+ // order.
+ clang::SourceLocation insertLoc =
+ source_manager_.getSpellingLoc(std::get<1>(includes.front()));
+
+ // Emit the fix-it hint to add the include statement.
+ // TODO: consider using std::format after moving to C++20
+ std::string FixText = "#include \"" + include_header + "\"\n";
+ clang::FixItHint FixIt =
+ clang::FixItHint::CreateInsertion(insertLoc, FixText);
+ diagnostics_engine.Report(insertLoc, kID) << include_header << FixIt;
+
+ // Add the new include to our list so we don't add it again.
+ includes.insert(
+ includes.begin(),
+ std::tuple(static_cast<std::string>(include_header), insertLoc));
+ }
+
+ clang::DiagnosticBuilder
+ unexported_public_interface(const clang::Decl *D) {
+ return unexported_public_interface(D, get_location(D));
+ }
+
+ clang::DiagnosticBuilder
+ unexported_public_interface(const clang::Decl *D, clang::SourceLocation location) {
+ // Track every unexported declaration encountered, even when filtered, so
+ // is_symbol_exported can avoid double-reporting the same symbol.
+ exported_decls_.insert(D);
+
+ if (!is_in_target_headers(location))
+ return discarded_diagnostic(location);
+
+ add_missing_include(location);
+
+ clang::DiagnosticsEngine &diagnostics_engine = context_.getDiagnostics();
+
+ if (!id_unexported_)
+ id_unexported_ =
+ diagnostics_engine.getCustomDiagID(clang::DiagnosticsEngine::Remark,
+ "unexported public interface %0");
+
+ return diagnostics_engine.Report(location, *id_unexported_);
+ }
+
+ clang::DiagnosticBuilder
+ improperly_exported_interface(clang::SourceLocation location) {
+ if (!is_in_target_headers(location))
+ return discarded_diagnostic(location);
+
+ clang::DiagnosticsEngine &diagnostics_engine = context_.getDiagnostics();
+
+ if (!id_improper_)
+ id_improper_ = diagnostics_engine.getCustomDiagID(
+ clang::DiagnosticsEngine::Remark,
+ "improperly exported symbol %0: %1");
+
+ return diagnostics_engine.Report(location, *id_improper_);
+ }
+
+ clang::DiagnosticBuilder
+ exported_private_interface(clang::SourceLocation location) {
+ if (!is_in_target_headers(location))
+ return discarded_diagnostic(location);
+
+ clang::DiagnosticsEngine &diagnostics_engine = context_.getDiagnostics();
+
+ if (!id_exported_)
+ id_exported_ =
+ diagnostics_engine.getCustomDiagID(clang::DiagnosticsEngine::Remark,
+ "exported private interface %0");
+
+ return diagnostics_engine.Report(location, *id_exported_);
+ }
+
+ template <typename Decl_>
+ inline clang::FullSourceLoc get_location(const Decl_ *TD) const {
+ return context_.getFullLoc(TD->getBeginLoc()).getExpansionLoc();
+ }
+
+ // Returns true if `loc` is inside one of the headers passed via --header.
+ // When --header is unset (legacy / unit-test path), every location matches.
+ // Path matching is suffix-based: `--header llvm/Support/Foo.h` matches a
+ // diagnostic whose absolute path ends with `llvm/Support/Foo.h`.
+ bool is_in_target_headers(clang::SourceLocation loc) const {
+ if (headers.empty())
+ return true;
+ if (loc.isInvalid())
+ return false;
+ clang::SourceLocation spelling = source_manager_.getSpellingLoc(loc);
+ llvm::StringRef filename = source_manager_.getFilename(spelling);
+ if (filename.empty())
+ return false;
+ for (const std::string &h : headers)
+ if (filename.ends_with(h))
+ return true;
+ return false;
+ }
+
+ // Return an Ignored-level diagnostic so that `<<` chains and FixItHints
+ // attached by the caller silently no-op. Used to filter diagnostics out
+ // of files we don't care about (locations outside the target headers).
+ clang::DiagnosticBuilder discarded_diagnostic(clang::SourceLocation loc) {
+ clang::DiagnosticsEngine &diagnostics_engine = context_.getDiagnostics();
+ static unsigned kID = diagnostics_engine.getCustomDiagID(
+ clang::DiagnosticsEngine::Ignored, "");
+ return diagnostics_engine.Report(loc, kID);
+ }
+
+ template <typename Decl_>
+ bool is_in_header(const Decl_ *D) const {
+ const clang::FullSourceLoc location = get_location(D);
+ const clang::FileID id = source_manager_.getFileID(location);
+ if (const auto entry = source_manager_.getFileEntryRefForID(id)) {
+ const llvm::StringRef name = entry->getName();
+ for (const auto &extension : {".h", ".hh", ".hpp", ".hxx"})
+ if (name.ends_with(extension))
+ return true;
+ }
+ return false;
+ }
+
+ template <typename Decl_>
+ inline bool is_in_system_header(const Decl_ *D) const {
+ return source_manager_.isInSystemHeader(get_location(D));
+ }
+
+ template <typename Decl_>
+ bool is_symbol_exported(const Decl_ *D) const {
+ // Check the set of symbols we've already marked for export.
+ if (exported_decls_.contains(D))
+ return true;
+
+ // Check if the symbol is annotated with __declspec(dllimport) or
+ // __declspec(dllexport).
+ if (D->template hasAttr<clang::DLLExportAttr>() ||
+ D->template hasAttr<clang::DLLImportAttr>())
+ return true;
+
+ // Check if the symbol is annotated with [[gnu::visibility("default")]]
+ // or the equivalent __attribute__((visibility("default")))
+ if (const auto *VA = D->template getAttr<clang::VisibilityAttr>())
+ return VA->getVisibility() == clang::VisibilityAttr::VisibilityType::Default;
+
+ return false;
+ }
+
+ template <typename Decl_>
+ bool is_containing_record_exported(const Decl_ *D) const {
+ // For non-record declarations, the DeclContext is the containing record.
+ for (const clang::DeclContext *DC = D->getDeclContext(); DC; DC = DC->getParent())
+ if (const auto *RD = llvm::dyn_cast<clang::RecordDecl>(DC))
+ return is_symbol_exported(RD);
+
+ return false;
+ }
+
+ // Emit a FixIt if a symbol is annotated with a default visibility or DLL
+ // export/import annotation. The FixIt will remove the annotation
+ template <typename Decl_>
+ void check_symbol_not_exported(const Decl_ *D, const std::string &message) {
+ clang::SourceLocation SLoc;
+ if (const auto *A = D->template getAttr<clang::DLLExportAttr>())
+ if (!A->isInherited())
+ SLoc = A->getLocation();
+
+ if (const auto *A = D->template getAttr<clang::DLLImportAttr>())
+ if (!A->isInherited())
+ SLoc = A->getLocation();
+
+ if (const auto *A = D->template getAttr<clang::VisibilityAttr>())
+ if (!A->isInherited() &&
+ A->getVisibility() == clang::VisibilityAttr::VisibilityType::Default)
+ SLoc = A->getLocation();
+
+ if (SLoc.isInvalid())
+ return;
+
+ if (SLoc.isMacroID())
+ SLoc = source_manager_.getExpansionLoc(SLoc);
+
+ clang::CharSourceRange range =
+ clang::CharSourceRange::getTokenRange(SLoc, SLoc);
+ improperly_exported_interface(SLoc)
+ << D << message << clang::FixItHint::CreateRemoval(range);
+ }
+
+ // Determine if a function needs exporting and add the export annotation as
+ // required.
+ void export_function_if_needed(const clang::FunctionDecl *FD) {
+ // Ignore declarations from the system.
+ if (is_in_system_header(FD))
+ return;
+
+ // Skip declarations not in header files.
+ if (!is_in_header(FD))
+ return;
+
+ // Ignore friend declarations.
+ if (FD->getFriendObjectKind() != clang::Decl::FOK_None)
+ return;
+
+ // Ignore known forward declarations (builtins)
+ if (contains(kIgnoredBuiltins, FD->getNameAsString()))
+ return;
+
+ // Skip functions contained in classes that are already exported.
+ if (is_containing_record_exported(FD)) {
+ // Exporting a symbol contained in an already exported class/struct will
+ // fail compilation on Windows.
+ check_symbol_not_exported(FD, "containing class is exported");
+ return;
+ }
+
+ // Skip any function defined inline, it can be materialized by the user.
+ if (FD->isThisDeclarationADefinition()) {
+ check_symbol_not_exported(FD, "function is defined inline");
+ return;
+ }
+
+ // Pure virtual methods cannot be exported.
+ if (const auto *MD = llvm::dyn_cast<clang::CXXMethodDecl>(FD))
+ if (MD->isPureVirtual()) {
+ check_symbol_not_exported(FD, "pure virtual method");
+ return;
+ }
+
+ // Ignore deleted and defaulted functions (e.g. operators).
+ if (FD->isDeleted() || FD->isDefaulted())
+ return;
+
+ // We are only interested in non-dependent types.
+ if (FD->isDependentContext())
+ return;
+
+ // Skip methods in template declarations.
+ if (FD->getTemplateInstantiationPattern())
+ return;
+
+ // Skip template class template argument deductions.
+ if (llvm::isa<clang::CXXDeductionGuideDecl>(FD))
+ return;
+
+ // If the function has an inline body in a header, callers can
+ // materialize the symbol locally and the function does not need an
+ // export annotation. Out-of-line definitions in a .cpp DO need export:
+ // external callers in other translation units only see the header
+ // declaration, not the .cpp body, even when both happen to be visible
+ // in the TU we are currently checking. The original tool conflated
+ // these cases via FD->hasBody(); we use FunctionDecl::isOutOfLine()
+ // on the active definition to disambiguate.
+ if (const clang::FunctionDecl *Def = FD->getDefinition())
+ if (!Def->isOutOfLine())
+ return;
+
+ // Check if the symbol is already exported.
+ if (is_symbol_exported(FD))
+ return;
+
+ // Use the inner start location so that the annotation comes after
+ // any template information.
+ clang::SourceLocation SLoc = FD->getInnerLocStart();
+
+ // If the function declaration has any existing attributes, the export macro
+ // should be inserted after them. We can approximate this location using the
+ // function's return type location.
+ if (!FD->attrs().empty())
+ SLoc = FD->getTypeSourceInfo()->getTypeLoc().getBeginLoc();
+
+ unexported_public_interface(FD, SLoc)
+ << FD << clang::FixItHint::CreateInsertion(SLoc, export_macro + " ");
+ }
+
+ // Determine if a variable needs exporting and add the export annotation as
+ // required. This only applies to extern globals and static member fields.
+ void export_variable_if_needed(const clang::VarDecl *VD) {
+ // Ignore declarations from the system.
+ if (is_in_system_header(VD))
+ return;
+
+ // Skip all variable declarations not in header files.
+ if (!is_in_header(VD))
+ return;
+
+ // Skip local variables. We are only interested in static fields.
+ if (VD->getParentFunctionOrMethod())
+ return;
+
+ // Skip variables that have initializers.
+ if (VD->hasInit()) {
+ check_symbol_not_exported(VD, "variable initialized at declaration");
+ return;
+ }
+
+ // Skip all other local and global variables unless they are extern.
+ if (!(VD->isStaticDataMember() ||
+ VD->getStorageClass() == clang::StorageClass::SC_Extern)) {
+ check_symbol_not_exported(VD, "variable not static or extern");
+ return;
+ }
+
+ // Skip variables contained in classes that are already exported.
+ if (is_containing_record_exported(VD)) {
+ check_symbol_not_exported(VD, "containing class is exported");
+ return;
+ }
+
+ // Skip static variables declared in template class unless the template is
+ // fully specialized.
+ if (auto *RD = llvm::dyn_cast<clang::CXXRecordDecl>(VD->getDeclContext())) {
+ if (RD->getDescribedClassTemplate())
+ return;
+
+ if (auto *CTSD = llvm::dyn_cast<clang::ClassTemplateSpecializationDecl>(RD))
+ if (llvm::isa<clang::ClassTemplatePartialSpecializationDecl>(CTSD))
+ return;
+ }
+
+ // Skip fields in template declarations.
+ if (VD->getTemplateInstantiationPattern() != nullptr)
+ return;
+
+ // Check if the symbol is already exported.
+ if (is_symbol_exported(VD))
+ return;
+
+ clang::SourceLocation SLoc = VD->getBeginLoc();
+
+ // If the variable declaration has any existing attributes, the export macro
+ // should be inserted after them. Similarly, if the variable has external
+ // storage, the export macro should be inserted after the extern keyword. We
+ // can approximate this location using the variable's type location.
+ if (!VD->attrs().empty() || VD->hasExternalStorage())
+ SLoc = VD->getTypeSourceInfo()->getTypeLoc().getBeginLoc();
+
+ unexported_public_interface(VD, SLoc)
+ << VD << clang::FixItHint::CreateInsertion(SLoc, export_macro + " ");
+ }
+
+ // Determine if a tagged type needs exporting at the record level and add the
+ // export annotation as required.
+ void export_record_if_needed(clang::CXXRecordDecl *RD) {
+ // Check if the class is already exported.
+ if (is_symbol_exported(RD))
+ return;
+
+ // Ignore declarations from the system.
+ if (is_in_system_header(RD))
+ return;
+
+ // Skip exporting template classes. For fully-specialized template classes,
+ // isTemplated() returns false so they will be annotated if needed.
+ if (RD->isTemplated())
+ return;
+
+ // If a class declaration contains an out-of-line virtual method, annotate
+ // the class instead of its individual members. This ensures its vtable is
+ // exported on non-Windows platforms. Do this regardless of the method's
+ // access level.
+ bool should_export_record = false;
+ for (const auto *MD : RD->methods())
+ if ((should_export_record =
+ !(MD->isPureVirtual() || MD->isDefaulted() || MD->isDeleted()) &&
+ (MD->isVirtual() && !MD->hasBody())))
+ break;
+
+ if (!should_export_record)
+ return;
+
+ // Insert the annotation immediately before the tag name, which is the
+ // position returned by getLocation.
+ clang::LangOptions LO = RD->getASTContext().getLangOpts();
+ clang::SourceLocation SLoc = RD->getQualifier()
+ ? RD->getQualifierLoc().getBeginLoc()
+ : RD->getLocation();
+ const clang::SourceLocation location =
+ context_.getFullLoc(SLoc).getExpansionLoc();
+ unexported_public_interface(RD, location)
+ << RD << clang::FixItHint::CreateInsertion(SLoc, export_macro + " ");
+ }
+
+public:
+ visitor(clang::ASTContext &context, PPCallbacks::FileIncludes &file_includes)
+ : context_(context), source_manager_(context.getSourceManager()),
+ file_includes_(file_includes) {}
+
+ bool TraverseCXXRecordDecl(clang::CXXRecordDecl *RD) {
+ export_record_if_needed(RD);
+
+ // Traverse the class by invoking the parent's version of this method. This
+ // call is required even if the record is exported because it may contain
+ // nested records.
+ return RecursiveASTVisitor::TraverseCXXRecordDecl(RD);
+ }
+
+ // RecursiveASTVisitor::TraverseCXXRecordDecl does not get called for fully
+ // specialized template declarations. Since we may want to export them,
+ // manually invoke TraverseCXXRecordDecl whenever an explicit specialization
+ // is found.
+ bool TraverseClassTemplateSpecializationDecl(
+ clang::ClassTemplateSpecializationDecl *SD) {
+ switch (SD->getSpecializationKind()) {
+ case clang::TSK_ExplicitSpecialization:
+ // This call visits class template specialization record and recursively
+ // visits all of it children, which may also require export.
+ return TraverseCXXRecordDecl(SD);
+
+ // TODO: consider annotating explicit template instantiation declarations
+ // and definitions in the future. They may require unique annotation macros
+ // due to differences between visibility and dllexport/dllimport attributes.
+ case clang::TSK_ExplicitInstantiationDeclaration:
+ [[fallthrough]];
+ case clang::TSK_ExplicitInstantiationDefinition:
+ [[fallthrough]];
+ default:
+ return true;
+ }
+ }
+
+ // VisitFunctionDecl will visit all function declarations. This includes top-
+ // level functions as well as class member and static functions.
+ bool VisitFunctionDecl(clang::FunctionDecl *FD) {
+ // Ignore private member function declarations. Any that require export will
+ // be identified by VisitCallExpr.
+ if (const auto *MD = llvm::dyn_cast<clang::CXXMethodDecl>(FD))
+ if (MD->getAccess() == clang::AccessSpecifier::AS_private)
+ return true;
+
+ export_function_if_needed(FD);
+ return true;
+ }
+
+ // Visit every function call in the compilation unit to determine if there are
+ // any inline calls to private member functions. In this uncommon case, the
+ // private method must be annotated for export.
+ bool VisitCallExpr(clang::CallExpr *CE) {
+ const clang::FunctionDecl *FD = CE->getDirectCallee();
+ if (!FD)
+ return true;
+
+ const clang::CXXMethodDecl *MD = llvm::dyn_cast<clang::CXXMethodDecl>(FD);
+ if (!MD)
+ return true;
+
+ // Only consider private methods here. Non-private methods will be
+ // considered for export by VisitFunctionDecl.
+ if (MD->getAccess() == clang::AccessSpecifier::AS_private)
+ export_function_if_needed(MD);
+
+ return true;
+ }
+
+ // Visit every unresolved member expression in the compilation unit to
+ // determine if there are overloaded private methods that might be called. In
+ // this uncommon case, the private method should be annotated.
+ bool VisitUnresolvedMemberExpr(clang::UnresolvedMemberExpr *E) {
+ // Iterate over potential declarations
+ for (const clang::NamedDecl *ND : E->decls())
+ if (const auto *MD = llvm::dyn_cast<clang::CXXMethodDecl>(ND))
+ if (MD->getAccess() == clang::AccessSpecifier::AS_private)
+ export_function_if_needed(MD);
+
+ return true;
+ }
+
+ // Visit every constructor call in the compilation unit to determine if there
+ // are any inline calls to private constructors. In this uncommon case, the
+ // private constructor must be annotated for export. Constructor calls are not
+ // visited by VisitCallExpr.
+ bool VisitCXXConstructExpr(clang::CXXConstructExpr *CE) {
+ const clang::CXXConstructorDecl *CD = CE->getConstructor();
+ if (!CD)
+ return true;
+
+ // Only consider private constructors here. Non-private constructors will be
+ // considered for export by VisitFunctionDecl.
+ if (CD->getAccess() == clang::AccessSpecifier::AS_private)
+ export_function_if_needed(CD);
+
+ return true;
+ }
+
+ // VisitVarDecl will visit all variable declarations as well as static fields
+ // in classes and structs. Non-static fields are not visited by this method.
+ bool VisitVarDecl(clang::VarDecl *VD) {
+ // Ignore private static field declarations. Any that require export will be
+ // identified by VisitDeclRefExpr.
+ if (VD->getAccess() == clang::AccessSpecifier::AS_private)
+ return true;
+
+ export_variable_if_needed(VD);
+ return true;
+ }
+
+ // Visit every variable reference in the compilation unit to determine if
+ // there are any inline references to private, static member fields. In this
+ // uncommon case, the private field must be annotated for export.
+ bool VisitDeclRefExpr(clang::DeclRefExpr *DRE) {
+ // Only consider expresions referencing variable declarations. This includes
+ // static fields and local variables but not member variables, which are
+ // type FieldDecl.
+ auto *VD = llvm::dyn_cast<clang::VarDecl>(DRE->getDecl());
+ if (!VD)
+ return true;
+
+ // Only consider private fields here. Non-private fields will be considered
+ // for export by VisitVarDecl.
+ if (VD->getAccess() != clang::AccessSpecifier::AS_private)
+ return true;
+
+ export_variable_if_needed(VD);
+ return true;
+ }
+};
+
+class consumer : public clang::ASTConsumer {
+ struct fixit_options : clang::FixItOptions {
+ fixit_options() {
+ InPlace = inplace;
+ Silent = apply_fixits;
+ }
+
+ std::string RewriteFilename(const std::string &filename, int &fd) override {
+ llvm_unreachable("unexpected call to RewriteFilename");
+ }
+ };
+
+ idt::visitor visitor_;
+
+ fixit_options options_;
+ std::unique_ptr<clang::FixItRewriter> rewriter_;
+
+public:
+ consumer(clang::ASTContext &context, PPCallbacks::FileIncludes &file_includes)
+ : visitor_(context, file_includes) {}
+
+ void HandleTranslationUnit(clang::ASTContext &context) override {
+ if (apply_fixits) {
+ clang::DiagnosticsEngine &diagnostics_engine = context.getDiagnostics();
+ rewriter_ =
+ std::make_unique<clang::FixItRewriter>(diagnostics_engine,
+ context.getSourceManager(),
+ context.getLangOpts(),
+ &options_);
+ diagnostics_engine.setClient(rewriter_.get(), /*ShouldOwnClient=*/false);
+ }
+
+ visitor_.TraverseDecl(context.getTranslationUnitDecl());
+
+ if (apply_fixits)
+ rewriter_->WriteFixedFiles();
+ }
+};
+
+struct action : clang::ASTFrontendAction {
+ void ExecuteAction() override {
+ if (!include_header.empty())
+ installPPCallbacks();
+ clang::ASTFrontendAction::ExecuteAction();
+ }
+
+ std::unique_ptr<clang::ASTConsumer>
+ CreateASTConsumer(clang::CompilerInstance &CI, llvm::StringRef) override {
+ return std::make_unique<idt::consumer>(CI.getASTContext(), file_includes_);
+ }
+
+private:
+ // Install a callback that will be invoked on every preprocessor include
+ // statement. This is done so we can determine if a user-specified custom
+ // include statment needs to be added if any annotations are added.
+ void installPPCallbacks() {
+ clang::CompilerInstance &compiler_instance = getCompilerInstance();
+ clang::Preprocessor &preprocessor = compiler_instance.getPreprocessor();
+ clang::SourceManager &source_manager = compiler_instance.getSourceManager();
+ preprocessor.addPPCallbacks(
+ std::make_unique<PPCallbacks>(source_manager, file_includes_));
+ }
+
+ PPCallbacks::FileIncludes file_includes_;
+};
+
+struct factory : clang::tooling::FrontendActionFactory {
+ std::unique_ptr<clang::FrontendAction> create() override {
+ return std::make_unique<idt::action>();
+ }
+};
+}
+
+int main(int argc, char *argv[]) {
+ using namespace clang::tooling;
+
+ auto options =
+ CommonOptionsParser::create(argc, const_cast<const char **>(argv),
+ idt::category, llvm::cl::OneOrMore);
+ if (!options) {
+ llvm::logAllUnhandledErrors(std::move(options.takeError()), llvm::errs());
+ return EXIT_FAILURE;
+ }
+
+ // Resolve the LLVM category from --header. All listed headers must share
+ // the same category; otherwise the per-category compile flags (export
+ // macro, include header, system-header-prefix list) would conflict.
+ // `find_llvm_category` and the related globals live in this TU's anonymous
+ // namespace and are reachable here without a qualifier.
+ for (const std::string &h : headers) {
+ const llvm_category *cat = find_llvm_category(h);
+ if (!cat) {
+ llvm::errs() << "idt: --header '" << h
+ << "' is outside the known LLVM categories.\n";
+ return EXIT_FAILURE;
+ }
+ if (active_category && active_category != cat) {
+ llvm::errs() << "idt: --header values span multiple LLVM categories ("
+ << active_category->path_match << " vs. "
+ << cat->path_match
+ << "); batch headers per-category instead.\n";
+ return EXIT_FAILURE;
+ }
+ active_category = cat;
+ }
+
+ // Populate the per-category strings consumed by the visitor.
+ export_macro = active_category->export_macro.str();
+ include_header = active_category->include_header.str();
+
+ // Build the list of extra clang args we need to inject:
+ // -DLLVM_ABI=__attribute__((visibility("default"))) (or LLVM_C_ABI / DEMANGLE_ABI)
+ // -Wno-macro-redefined
+ // -Xclang --system-header-prefix=<prefix> (per category)
+ // -Xclang --no-system-header-prefix=<prefix> (per category)
+ std::vector<std::string> extra_args;
+ extra_args.push_back("-D" + export_macro +
+ "=__attribute__((visibility(\"default\")))");
+ extra_args.push_back("-Wno-macro-redefined");
+ for (llvm::StringRef p : active_category->system_header_prefixes) {
+ extra_args.push_back("-Xclang");
+ extra_args.push_back(("--system-header-prefix=" + p).str());
+ }
+ for (llvm::StringRef p : active_category->no_system_header_prefixes) {
+ extra_args.push_back("-Xclang");
+ extra_args.push_back(("--no-system-header-prefix=" + p).str());
+ }
+
+ ClangTool tool{options->getCompilations(), options->getSourcePathList()};
+ tool.appendArgumentsAdjuster(getInsertArgumentAdjuster(
+ extra_args, ArgumentInsertPosition::END));
+ return tool.run(new idt::factory{});
+}
More information about the llvm-commits
mailing list