[llvm] [FileCheck] Add check-not support for diff option (PR #189357)

Shivam Gupta via llvm-commits llvm-commits at lists.llvm.org
Sat Apr 18 04:51:41 PDT 2026


https://github.com/xgupta updated https://github.com/llvm/llvm-project/pull/189357

>From 41e32c75d74f0b8e12609311e7a8ef20fea38b34 Mon Sep 17 00:00:00 2001
From: Shivam Gupta <shivam98.tkg at gmail.com>
Date: Sat, 18 Apr 2026 16:58:24 +0530
Subject: [PATCH 1/2] [FileCheck] Add variable substitution for diff option

---
 llvm/docs/CommandGuide/FileCheck.rst          |   5 +
 llvm/include/llvm/FileCheck/FileCheck.h       |   8 +-
 llvm/lib/FileCheck/FileCheck.cpp              | 147 ++++++++++++++++--
 llvm/lib/FileCheck/FileCheckImpl.h            |   2 +
 .../FileCheck/diff/diff-label-fuzzy-match.txt |  19 ++-
 .../FileCheck/diff/diff-long-line-trunc.txt   |  12 ++
 llvm/test/FileCheck/diff/diff-multi-subs.txt  |  15 ++
 .../FileCheck/diff/diff-nested-vars-subs.txt  |  16 ++
 .../test/FileCheck/diff/diff-numeric-expr.txt |  16 ++
 .../diff/diff-resync-after-noise.txt          |  19 ++-
 .../FileCheck/diff/diff-substitutions.txt     |  23 +++
 .../diff/diff-variable-chain-subs.txt         |  18 +++
 llvm/utils/FileCheck/FileCheck.cpp            |   7 +-
 13 files changed, 276 insertions(+), 31 deletions(-)
 create mode 100644 llvm/test/FileCheck/diff/diff-long-line-trunc.txt
 create mode 100644 llvm/test/FileCheck/diff/diff-multi-subs.txt
 create mode 100644 llvm/test/FileCheck/diff/diff-nested-vars-subs.txt
 create mode 100644 llvm/test/FileCheck/diff/diff-numeric-expr.txt
 create mode 100644 llvm/test/FileCheck/diff/diff-substitutions.txt
 create mode 100644 llvm/test/FileCheck/diff/diff-variable-chain-subs.txt

diff --git a/llvm/docs/CommandGuide/FileCheck.rst b/llvm/docs/CommandGuide/FileCheck.rst
index d99cbbdd9f36f..4e6d8eb369295 100644
--- a/llvm/docs/CommandGuide/FileCheck.rst
+++ b/llvm/docs/CommandGuide/FileCheck.rst
@@ -117,6 +117,11 @@ and from the command line.
 
   * ``none`` – Use the standard FileCheck diagnostic messages.
   * ``unidiff`` – Display mismatches using a unified diff format.
+  * ``split`` – Display mismatches using a side-by-side split diff view.
+  * ``unidiff-no-substitutions`` – Same as ``unidiff`` but shows the raw
+    pattern without applying variable substitutions.
+  * ``split-no-substitutions`` – Same as ``split`` but shows the raw
+    pattern without applying variable substitutions.
 
 .. option:: --dump-input <value>
 
diff --git a/llvm/include/llvm/FileCheck/FileCheck.h b/llvm/include/llvm/FileCheck/FileCheck.h
index 7896a942a3466..446447aca7d3f 100644
--- a/llvm/include/llvm/FileCheck/FileCheck.h
+++ b/llvm/include/llvm/FileCheck/FileCheck.h
@@ -28,7 +28,13 @@ class SourceMgr;
 template <typename T> class SmallVectorImpl;
 
 // Diff the output on failures.
-enum DiffFormatType { None, Unified };
+enum DiffFormatType {
+  None,
+  Unified,
+  Split,
+  SplitNoSubstitution,
+  UnifiedNoSubstitution
+};
 
 /// Contains info about various FileCheck options.
 struct FileCheckRequest {
diff --git a/llvm/lib/FileCheck/FileCheck.cpp b/llvm/lib/FileCheck/FileCheck.cpp
index 307be0b6e2faf..243ded6362234 100644
--- a/llvm/lib/FileCheck/FileCheck.cpp
+++ b/llvm/lib/FileCheck/FileCheck.cpp
@@ -2762,11 +2762,82 @@ static DiffContext getDiffContext(SourceMgr &SM, unsigned LineNo,
           getLineText(LineNo + 1)};
 }
 
+// Extracts a fixed-width "window" from string S, centered around DiffPos.
+static std::string getCenteredView(StringRef S, size_t DiffPos, size_t Width) {
+  if (S.size() <= Width)
+    return S.str() + std::string(Width - S.size(), ' ');
+
+  size_t HalfWidth = Width / 2;
+  size_t Start = (DiffPos > HalfWidth) ? DiffPos - HalfWidth : 0;
+
+  if (Start + Width > S.size())
+    Start = (S.size() > Width) ? S.size() - Width : 0;
+
+  std::string View = S.substr(Start, Width).str();
+
+  if (Start > 0 && Width > 3)
+    View.replace(0, 3, "...");
+
+  if (Start + Width < S.size() && View.size() > 3)
+    View.replace(View.size() - 3, 3, "...");
+
+  return View;
+}
+
+std::string Pattern::getSubstitutedRegex(StringRef PatternText) const {
+  std::string Result = PatternText.str();
+
+  // Iterate through substitutions and replace tags from left to right.
+  for (const auto &Substitution : Substitutions) {
+    Expected<std::string> ValueOrErr = Substitution->getResultForDiagnostics();
+    std::string CleanValue;
+
+    if (!ValueOrErr) {
+      consumeError(ValueOrErr.takeError());
+      CleanValue = "<UNDEFINED>";
+    } else {
+      CleanValue = *ValueOrErr;
+      // Numeric substitutions arrive wrapped in quotes so we need to strip
+      // them.
+      if (CleanValue.size() >= 2 && CleanValue.front() == '"' &&
+          CleanValue.back() == '"')
+        CleanValue = CleanValue.substr(1, CleanValue.size() - 2);
+    }
+
+    // Find the first occurrence of a variable tag
+    size_t Start = Result.find("[[");
+    if (Start == std::string::npos)
+      break;
+
+    size_t End = Result.find("]]", Start);
+    if (End == std::string::npos)
+      break;
+
+    size_t TagLen = (End + 2) - Start;
+    Result.replace(Start, TagLen, CleanValue);
+  }
+  return Result;
+}
+
 // Renders a diagnostic diff via llvm::errs().
-static void renderDiff(unsigned ExpectedLineNo, unsigned ActualLineNo,
-                       StringRef ExpectedLine, StringRef ActualLine,
-                       const DiffContext &Ctx) {
+static void renderDiff(DiffFormatType Mode, unsigned ExpectedLineNo,
+                       unsigned ActualLineNo, StringRef ExpectedLine,
+                       StringRef ActualLine, const DiffContext &Ctx) {
   auto &OS = errs();
+  constexpr unsigned ColWidth = 45;
+  constexpr StringRef Sep = " | ";
+
+  bool IsSplit = (Mode == Split || Mode == SplitNoSubstitution);
+
+  // Find the index from where the expected and actual text diverge.
+  size_t DiffPos = 0;
+  size_t MinLen = std::min(ExpectedLine.size(), ActualLine.size());
+  while (DiffPos < MinLen && ExpectedLine[DiffPos] == ActualLine[DiffPos])
+    ++DiffPos;
+
+  auto GetView = [&](StringRef S) {
+    return getCenteredView(S, DiffPos, ColWidth - 1);
+  };
 
   // Header
   OS.changeColor(raw_ostream::CYAN);
@@ -2775,31 +2846,71 @@ static void renderDiff(unsigned ExpectedLineNo, unsigned ActualLineNo,
 
   // Before Context
   if (!Ctx.LineBefore.empty()) {
-    OS << " " << Ctx.LineBefore << "\n";
+    if (IsSplit)
+      OS << "  " << GetView(Ctx.LineBefore) << Sep << GetView(Ctx.LineBefore)
+         << "\n";
+    else
+      OS << " " << Ctx.LineBefore << "\n";
   }
 
   // Mismatch
-  OS.changeColor(raw_ostream::RED);
-  OS << "-" << ExpectedLine << "\n";
+  if (IsSplit) {
+    OS << "  ";
 
-  OS.changeColor(raw_ostream::GREEN);
-  OS << "+" << ActualLine.ltrim() << "\n";
-  OS.resetColor();
+    OS.changeColor(raw_ostream::RED);
+    OS << GetView(ExpectedLine);
+    OS.resetColor();
+
+    // Use a bold yellow '!' to signify that the text might match,
+    // but it was found on the wrong line.
+    bool IsMoved = (ExpectedLineNo != ActualLineNo);
+
+    if (IsMoved) {
+      OS.changeColor(raw_ostream::YELLOW, true);
+      OS << " ! ";
+    } else {
+      OS << " | ";
+    }
+    OS.resetColor();
+
+    OS.changeColor(raw_ostream::GREEN);
+    OS << StringRef(GetView(ActualLine)).ltrim() << "\n";
+    OS.resetColor();
+  } else {
+    OS.changeColor(raw_ostream::RED);
+    OS << "-" << ExpectedLine << "\n";
+
+    OS.changeColor(raw_ostream::GREEN);
+    OS << "+" << ActualLine.ltrim() << "\n";
+    OS.resetColor();
+  }
 
   // After Context
   if (!Ctx.LineAfter.empty()) {
-    OS << " " << Ctx.LineAfter << "\n";
+    if (Mode == Split)
+      OS << "  " << GetView(Ctx.LineAfter) << Sep << GetView(Ctx.LineAfter)
+         << "\n";
+    else
+      OS << " " << Ctx.LineAfter << "\n";
   }
 }
 
-static bool printDiff(const FileCheckString &CheckStr, StringRef ActualLine,
-                      SourceMgr &SM, std::vector<FileCheckDiag> *Diags,
+static bool printDiff(DiffFormatType Mode, const FileCheckString &CheckStr,
+                      StringRef ActualLine, SourceMgr &SM,
+                      std::vector<FileCheckDiag> *Diags,
                       unsigned OverwriteActualLine = 0) {
   SMLoc PatternLoc = CheckStr.Pat.getLoc();
   unsigned ExpectedLineNo = SM.getLineAndColumn(PatternLoc).first;
   const char *PatPtr = PatternLoc.getPointer();
   StringRef ExpectedLine = StringRef(PatPtr).split('\n').first.rtrim();
 
+  std::string ExpectedText;
+  if (Mode == DiffFormatType::SplitNoSubstitution ||
+      Mode == DiffFormatType::UnifiedNoSubstitution)
+    ExpectedText = ExpectedLine.str();
+  else
+    ExpectedText = CheckStr.Pat.getSubstitutedRegex(ExpectedLine);
+
   // Resolve the Actual (Input) line number.
   // Priority: 1. OverwriteActualLine (Found via Fuzzy match)
   //           2. Direct pointer resolution via SourceMgr.
@@ -2820,7 +2931,8 @@ static bool printDiff(const FileCheckString &CheckStr, StringRef ActualLine,
   unsigned BufID = SM.FindBufferContainingLoc(InputLoc);
   DiffContext Context = getDiffContext(SM, ActualLineNo, BufID);
 
-  renderDiff(ExpectedLineNo, ActualLineNo, ExpectedLine, ActualLine, Context);
+  renderDiff(Mode, ExpectedLineNo, ActualLineNo, ExpectedText, ActualLine,
+             Context);
 
   errs() << '\n';
   return true;
@@ -2829,6 +2941,7 @@ static bool printDiff(const FileCheckString &CheckStr, StringRef ActualLine,
 // Report the mismatch on the current line and advance to the next line.
 static bool handleDiffFailure(const FileCheckString &CheckStr,
                               StringRef &CheckRegion, SourceMgr &SM,
+                              FileCheckRequest &Req,
                               std::vector<FileCheckDiag> *Diags,
                               raw_ostream &OS, bool &HeaderPrinted,
                               unsigned &TotalMismatches) {
@@ -2874,7 +2987,7 @@ static bool handleDiffFailure(const FileCheckString &CheckStr,
     TargetLineNo = SM.getLineAndColumn(CurrentLoc).first;
   }
 
-  printDiff(CheckStr, TargetLine, SM, Diags, TargetLineNo);
+  printDiff(Req.DiffFormat, CheckStr, TargetLine, SM, Diags, TargetLineNo);
   TotalMismatches++;
 
   // Advance CheckRegion past the current line to recover for the next CHECK.
@@ -2940,8 +3053,8 @@ bool FileCheck::checkInput(SourceMgr &SM, StringRef Buffer,
       // Handle failure
       if (MatchPos == StringRef::npos) {
         if (IsDiffFormat) {
-          handleDiffFailure(CheckStr, CheckRegion, SM, Diags, OS, HeaderPrinted,
-                            TotalMismatches);
+          handleDiffFailure(CheckStr, CheckRegion, SM, Req, Diags, OS,
+                            HeaderPrinted, TotalMismatches);
         }
         ChecksFailed = true;
         i = j;
@@ -2957,7 +3070,7 @@ bool FileCheck::checkInput(SourceMgr &SM, StringRef Buffer,
             (CurrentLineEnd != StringRef::npos)
                 ? CheckRegion.drop_front(CurrentLineEnd + 1)
                 : CheckRegion;
-        handleDiffFailure(CheckStr, NextLineRegion, SM, Diags, OS,
+        handleDiffFailure(CheckStr, NextLineRegion, SM, Req, Diags, OS,
                           HeaderPrinted, TotalMismatches);
         ChecksFailed = true;
         i = j;
diff --git a/llvm/lib/FileCheck/FileCheckImpl.h b/llvm/lib/FileCheck/FileCheckImpl.h
index 58a03d280b658..5b47b87076638 100644
--- a/llvm/lib/FileCheck/FileCheckImpl.h
+++ b/llvm/lib/FileCheck/FileCheckImpl.h
@@ -740,6 +740,8 @@ class Pattern {
                        std::vector<FileCheckDiag> *Diags,
                        const FileCheckRequest &Req) const;
 
+  std::string getSubstitutedRegex(StringRef PatternText) const;
+
   bool hasVariable() const {
     return !(Substitutions.empty() && VariableDefs.empty());
   }
diff --git a/llvm/test/FileCheck/diff/diff-label-fuzzy-match.txt b/llvm/test/FileCheck/diff/diff-label-fuzzy-match.txt
index a04f2e84ac70b..1a011a61032ca 100644
--- a/llvm/test/FileCheck/diff/diff-label-fuzzy-match.txt
+++ b/llvm/test/FileCheck/diff/diff-label-fuzzy-match.txt
@@ -1,6 +1,8 @@
 ; RUN: split-file %s %t
 ; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=unidiff 2>&1 \
-; RUN:   | FileCheck %s --check-prefix=DIFF
+; RUN:   | FileCheck %s --check-prefix=UNI
+; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=split 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=SPLIT
 
 ;--- input.txt
 define void @foo() {
@@ -11,8 +13,13 @@ define void @foo() {
 ; CHECK-LABEL: define void @foo()
 ; CHECK:        call void @work_test()
 
-; DIFF:      @@ -[[#]] +[[#]] @@
-; DIFF:      define void @foo() {
-; DIFF-NEXT: -call void @work_test()
-; DIFF-NEXT: +call void @work_task()
-; DIFF-NEXT:  }
+; UNI:      @@ -[[#]] +[[#]] @@
+; UNI:      define void @foo() {
+; UNI-NEXT: -call void @work_test()
+; UNI-NEXT: +call void @work_task()
+; UNI-NEXT:  }
+
+; SPLIT:      @@ -[[#]] +[[#]] @@
+; SPLIT-NEXT: define void @foo() {              | define void @foo() {
+; SPLIT-NEXT: {{.*}}call void @work_test(){{.*}}|{{.*}}call void @work_task()
+; SPLIT-NEXT: }
diff --git a/llvm/test/FileCheck/diff/diff-long-line-trunc.txt b/llvm/test/FileCheck/diff/diff-long-line-trunc.txt
new file mode 100644
index 0000000000000..f854ff3956864
--- /dev/null
+++ b/llvm/test/FileCheck/diff/diff-long-line-trunc.txt
@@ -0,0 +1,12 @@
+; RUN: split-file %s %t
+; RUN: not FileCheck %t/check.txt --input-file=%t/input.txt --diff=split 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=DIFF
+
+;--- input.txt
+THIS_IS_A_VERY_LONG_PREFIX_THAT_SHOULD_BE_TRUNCATED_BY_THE_SLIDING_WINDOW_LOGIC_ACTUAL_VALUE
+
+;--- check.txt
+; CHECK: THIS_IS_A_VERY_LONG_PREFIX_THAT_SHOULD_BE_TRUNCATED_BY_THE_SLIDING_WINDOW_LOGIC_EXPECTED_VALUE
+
+; DIFF:      @@ -1 +1 @@
+; DIFF-NEXT: {{.*}}...{{.*}}WINDOW_LOGIC_EXPECTED_VALUE{{.*}}|{{.*}}...{{.*}}WINDOW_LOGIC_ACTUAL_VALUE
diff --git a/llvm/test/FileCheck/diff/diff-multi-subs.txt b/llvm/test/FileCheck/diff/diff-multi-subs.txt
new file mode 100644
index 0000000000000..40d72113a44a1
--- /dev/null
+++ b/llvm/test/FileCheck/diff/diff-multi-subs.txt
@@ -0,0 +1,15 @@
+; RUN: split-file %s %t
+; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=split 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=DIFF
+
+;--- input.txt
+SET REG=rax VAL=0
+USE rax, 1
+
+;--- check.txt
+; CHECK: SET REG=[[REG:[a-z]+]] VAL=[[VAL:[0-9]+]]
+; CHECK: USE [[REG]], [[VAL]]
+
+; DIFF:      @@ -2 +2 @@
+; DIFF-NEXT: {{.*}}SET REG=rax VAL=0{{.*}}| SET REG=rax VAL=0{{.*}}
+; DIFF-NEXT: {{.*}}USE rax, 0{{.*}}|{{.*}}USE rax, 1{{.*}}
diff --git a/llvm/test/FileCheck/diff/diff-nested-vars-subs.txt b/llvm/test/FileCheck/diff/diff-nested-vars-subs.txt
new file mode 100644
index 0000000000000..7be1c1d9d96ab
--- /dev/null
+++ b/llvm/test/FileCheck/diff/diff-nested-vars-subs.txt
@@ -0,0 +1,16 @@
+; RUN: split-file %s %t
+; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=unidiff 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=DIFF
+
+;--- input.txt
+SET [[42]]
+DATA: [[42]]
+
+;--- check.txt
+; CHECK: SET [[INNER:.*]]
+; CHECK: DATA: WRONG_VALUE
+
+; DIFF:      @@ -[[#]] +[[#]] @@
+; DIFF-NEXT:  SET {{\[\[}}42{{\]\]}}
+; DIFF-NEXT: -DATA: WRONG_VALUE
+; DIFF-NEXT: +DATA: {{\[\[}}42{{\]\]}}
diff --git a/llvm/test/FileCheck/diff/diff-numeric-expr.txt b/llvm/test/FileCheck/diff/diff-numeric-expr.txt
new file mode 100644
index 0000000000000..36cfe98bea0d0
--- /dev/null
+++ b/llvm/test/FileCheck/diff/diff-numeric-expr.txt
@@ -0,0 +1,16 @@
+; RUN: split-file %s %t
+; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=unidiff 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=DIFF
+
+;--- input.txt
+BASE: 10
+NEXT: 12
+
+;--- check.txt
+; CHECK: BASE: [[#VAL:]]
+; CHECK: NEXT: [[#VAL+1]]
+
+; DIFF:      @@ -[[#]] +[[#]] @@
+; DIFF-NEXT:  BASE: 10
+; DIFF-NEXT: -NEXT: 11
+; DIFF-NEXT: +NEXT: 12
diff --git a/llvm/test/FileCheck/diff/diff-resync-after-noise.txt b/llvm/test/FileCheck/diff/diff-resync-after-noise.txt
index 854ee188b2944..8fd4d73b9e83d 100644
--- a/llvm/test/FileCheck/diff/diff-resync-after-noise.txt
+++ b/llvm/test/FileCheck/diff/diff-resync-after-noise.txt
@@ -1,6 +1,8 @@
 ; RUN: split-file %s %t
 ; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=unidiff 2>&1 \
-; RUN:   | FileCheck %s --check-prefix=DIFF
+; RUN:   | FileCheck %s --check-prefix=UNI
+; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=split 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=SPLIT
 
 ;--- input.txt
 HEADER
@@ -23,8 +25,13 @@ Noise6
 ; CHECK-NEXT: WRONG_NEXT
 ; CHECK:       END_OF_FUNCTION
 
-; DIFF:      @@ -[[#]] +[[#]] @@
-; DIFF-NEXT:  MATCH_ME
-; DIFF-NEXT: -WRONG_NEXT
-; DIFF-NEXT: +NEXT_LINE
-; DIFF-NEXT:  Noise5
+; UNI:      @@ -[[#]] +[[#]] @@
+; UNI-NEXT:  MATCH_ME
+; UNI-NEXT: -WRONG_NEXT
+; UNI-NEXT: +NEXT_LINE
+; UNI-NEXT:  Noise5
+
+; SPLIT:      @@ -[[#]] +[[#]] @@
+; SPLIT-NEXT: MATCH_ME              | MATCH_ME
+; SPLIT-NEXT: {{.*}}WRONG_NEXT{{.*}}!{{.*}}NEXT_LINE
+; SPLIT-NEXT: Noise5                | Noise5
diff --git a/llvm/test/FileCheck/diff/diff-substitutions.txt b/llvm/test/FileCheck/diff/diff-substitutions.txt
new file mode 100644
index 0000000000000..11c8a68484f1f
--- /dev/null
+++ b/llvm/test/FileCheck/diff/diff-substitutions.txt
@@ -0,0 +1,23 @@
+; RUN: split-file %s %t
+; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=unidiff 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=SUBST
+; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=unidiff-no-substitutions 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=RAW
+
+;--- input.txt
+START 42
+FAIL 100
+
+;--- check.txt
+; CHECK: START [[VAL:[0-9]+]]
+; CHECK: FAIL [[VAL]]
+
+; SUBST:      @@ -[[#]] +2 @@
+; SUBST-NEXT:  START 42
+; SUBST-NEXT: -FAIL 42
+; SUBST-NEXT: +FAIL 100
+
+; RAW:      @@ -[[#]] +2 @@
+; RAW-NEXT:  START 42
+; RAW-NEXT: -FAIL {{\[\[VAL\]\]}}
+; RAW-NEXT: +FAIL 100
diff --git a/llvm/test/FileCheck/diff/diff-variable-chain-subs.txt b/llvm/test/FileCheck/diff/diff-variable-chain-subs.txt
new file mode 100644
index 0000000000000..4e172a4f28789
--- /dev/null
+++ b/llvm/test/FileCheck/diff/diff-variable-chain-subs.txt
@@ -0,0 +1,18 @@
+; RUN: split-file %s %t
+; RUN: %ProtectFileCheckOutput not FileCheck %t/check.txt --input-file=%t/input.txt --diff=unidiff 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=DIFF
+
+;--- input.txt
+ID: 0x100
+TYPE: apple
+USE: 0x100 (orange)
+
+;--- check.txt
+; CHECK: ID: [[ID:0x[0-9a-f]+]]
+; CHECK: TYPE: [[TYPE:[a-z]+]]
+; CHECK: USE: [[ID]] ([[TYPE]])
+
+; DIFF:      @@ -[[#]] +[[#]] @@
+; DIFF-NEXT:  TYPE: apple
+; DIFF-NEXT: -USE: 0x100 (apple)
+; DIFF-NEXT: +USE: 0x100 (orange)
diff --git a/llvm/utils/FileCheck/FileCheck.cpp b/llvm/utils/FileCheck/FileCheck.cpp
index f200e94becaa2..bfb1e541d57ed 100644
--- a/llvm/utils/FileCheck/FileCheck.cpp
+++ b/llvm/utils/FileCheck/FileCheck.cpp
@@ -118,7 +118,12 @@ static cl::opt<DiffFormatType> DiffFormat(
     cl::ValueOptional,
     cl::values(clEnumValN(None, "none", "Standard FileCheck diagnostics"),
                clEnumValN(Unified, "unidiff", "Outputs a Unified diff"),
-               clEnumValN(Unified, "", "")));
+               clEnumValN(Unified, "", ""),
+               clEnumValN(Split, "split", "Outputs a Split diff"),
+               clEnumValN(UnifiedNoSubstitution, "unidiff-no-substitutions",
+                          "Outputs a Unified diff with no substitutions"),
+               clEnumValN(SplitNoSubstitution, "split-no-substitutions",
+                          "Outputs a Split diff with no substitutions")));
 
 // The order of DumpInputValue members affects their precedence, as documented
 // for -dump-input below.

>From 7a488fb616d7987e0eb15f0e814fb181ede762e8 Mon Sep 17 00:00:00 2001
From: Shivam Gupta <shivam98.tkg at gmail.com>
Date: Sat, 18 Apr 2026 17:21:14 +0530
Subject: [PATCH 2/2] [FileCheck] Add check-not support for diff option

---
 llvm/lib/FileCheck/FileCheck.cpp            | 71 +++++++++++++++++++--
 llvm/test/FileCheck/diff/diff-check-not.txt | 23 +++++++
 2 files changed, 88 insertions(+), 6 deletions(-)
 create mode 100644 llvm/test/FileCheck/diff/diff-check-not.txt

diff --git a/llvm/lib/FileCheck/FileCheck.cpp b/llvm/lib/FileCheck/FileCheck.cpp
index 243ded6362234..76eae4b60cc7c 100644
--- a/llvm/lib/FileCheck/FileCheck.cpp
+++ b/llvm/lib/FileCheck/FileCheck.cpp
@@ -2032,6 +2032,11 @@ bool FileCheck::readCheckFile(
   return false;
 }
 
+static bool printDiff(DiffFormatType Mode, const Pattern &Pat,
+                      StringRef ActualLine, SourceMgr &SM,
+                      std::vector<FileCheckDiag> *Diags, bool &HeaderPrinted,
+                      unsigned OverwriteActualLine = 0);
+
 /// Returns either (1) \c ErrorSuccess if there was no error or (2)
 /// \c ErrorReported if an error was reported, such as an unexpected match.
 static Error printMatch(bool ExpectedMatch, const SourceMgr &SM,
@@ -2061,6 +2066,28 @@ static Error printMatch(bool ExpectedMatch, const SourceMgr &SM,
   SMRange MatchRange = ProcessMatchResult(MatchTy, SM, Loc, Pat.getCheckTy(),
                                           Buffer, MatchResult.TheMatch->Pos,
                                           MatchResult.TheMatch->Len, Diags);
+
+  if (!ExpectedMatch && Req.DiffFormat != DiffFormatType::None) {
+    size_t Pos = MatchResult.TheMatch->Pos;
+    // Extract full line containing the match
+    size_t LineStart = Buffer.rfind('\n', Pos);
+    LineStart = (LineStart == StringRef::npos) ? 0 : LineStart + 1;
+
+    size_t LineEnd = Buffer.find('\n', Pos);
+    LineEnd = (LineEnd == StringRef::npos) ? Buffer.size() : LineEnd;
+
+    StringRef ActualLine = Buffer.substr(LineStart, LineEnd - LineStart);
+
+    bool DummyHeaderPrinted = false;
+    printDiff(Req.DiffFormat, Pat, ActualLine, const_cast<SourceMgr &>(SM),
+              Diags, DummyHeaderPrinted);
+
+    handleAllErrors(std::move(MatchResult.TheError),
+                    [](const ErrorInfoBase &) {});
+
+    return ErrorReported::reportedOrSuccess(true);
+  }
+
   if (Diags) {
     Pat.printSubstitutions(SM, Buffer, MatchRange, MatchTy, Diags, Req);
     Pat.printVariableDefs(SM, MatchTy, Diags);
@@ -2895,11 +2922,29 @@ static void renderDiff(DiffFormatType Mode, unsigned ExpectedLineNo,
   }
 }
 
-static bool printDiff(DiffFormatType Mode, const FileCheckString &CheckStr,
+static bool printDiff(DiffFormatType Mode, const Pattern &Pat,
                       StringRef ActualLine, SourceMgr &SM,
-                      std::vector<FileCheckDiag> *Diags,
-                      unsigned OverwriteActualLine = 0) {
-  SMLoc PatternLoc = CheckStr.Pat.getLoc();
+                      std::vector<FileCheckDiag> *Diags, bool &HeaderPrinted,
+                      unsigned OverwriteActualLine) {
+  // Print headers once per file.
+  if (!HeaderPrinted) {
+    StringRef CheckFile =
+        SM.getMemoryBuffer(SM.getMainFileID())->getBufferIdentifier();
+
+    unsigned InputBufID =
+        SM.FindBufferContainingLoc(SMLoc::getFromPointer(ActualLine.data()));
+    StringRef InputFile = SM.getMemoryBuffer(InputBufID)->getBufferIdentifier();
+
+    auto &OS = llvm::errs();
+    OS.changeColor(raw_ostream::WHITE, true);
+    OS << "--- " << CheckFile << "\n";
+    OS << "+++ " << InputFile << "\n";
+    OS.resetColor();
+
+    HeaderPrinted = true;
+  }
+
+  SMLoc PatternLoc = Pat.getLoc();
   unsigned ExpectedLineNo = SM.getLineAndColumn(PatternLoc).first;
   const char *PatPtr = PatternLoc.getPointer();
   StringRef ExpectedLine = StringRef(PatPtr).split('\n').first.rtrim();
@@ -2909,7 +2954,7 @@ static bool printDiff(DiffFormatType Mode, const FileCheckString &CheckStr,
       Mode == DiffFormatType::UnifiedNoSubstitution)
     ExpectedText = ExpectedLine.str();
   else
-    ExpectedText = CheckStr.Pat.getSubstitutedRegex(ExpectedLine);
+    ExpectedText = Pat.getSubstitutedRegex(ExpectedLine);
 
   // Resolve the Actual (Input) line number.
   // Priority: 1. OverwriteActualLine (Found via Fuzzy match)
@@ -2987,7 +3032,8 @@ static bool handleDiffFailure(const FileCheckString &CheckStr,
     TargetLineNo = SM.getLineAndColumn(CurrentLoc).first;
   }
 
-  printDiff(Req.DiffFormat, CheckStr, TargetLine, SM, Diags, TargetLineNo);
+  printDiff(Req.DiffFormat, CheckStr.Pat, TargetLine, SM, Diags, HeaderPrinted,
+            TargetLineNo);
   TotalMismatches++;
 
   // Advance CheckRegion past the current line to recover for the next CHECK.
@@ -3043,6 +3089,8 @@ bool FileCheck::checkInput(SourceMgr &SM, StringRef Buffer,
     for (; i != j; ++i) {
       const FileCheckString &CheckStr = CheckStrings[i];
 
+      size_t OldDiagSize = Diags ? Diags->size() : 0;
+
       bool IsStrict = CheckStr.Pat.getCheckTy() == Check::CheckNext ||
                       CheckStr.Pat.getCheckTy() == Check::CheckEmpty;
 
@@ -3050,6 +3098,17 @@ bool FileCheck::checkInput(SourceMgr &SM, StringRef Buffer,
       size_t MatchPos =
           CheckStr.Check(SM, CheckRegion, false, MatchLen, Req, Diags);
 
+      if (Diags && Diags->size() > OldDiagSize) {
+        // Check if the last diagnostic was a forbidden string match
+        if (Diags->back().MatchTy == FileCheckDiag::MatchFoundButExcluded) {
+          ChecksFailed = true;
+          TotalMismatches++;
+          HeaderPrinted = true;
+          i = j;
+          break;
+        }
+      }
+
       // Handle failure
       if (MatchPos == StringRef::npos) {
         if (IsDiffFormat) {
diff --git a/llvm/test/FileCheck/diff/diff-check-not.txt b/llvm/test/FileCheck/diff/diff-check-not.txt
new file mode 100644
index 0000000000000..7e25d0adec787
--- /dev/null
+++ b/llvm/test/FileCheck/diff/diff-check-not.txt
@@ -0,0 +1,23 @@
+; RUN: split-file %s %t
+; RUN: not FileCheck %t/check.txt --input-file=%t/input.txt --diff=unidiff 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=DIFF
+
+;--- input.txt
+START
+This is a forbidden string
+END
+
+;--- check.txt
+START
+; CHECK-NOT: forbidden string
+END
+
+; DIFF:      --- {{.*}}check.txt
+; DIFF-NEXT: +++ {{.*}}input.txt
+; DIFF-NEXT:  @@ -2 +2 @@
+; DIFF-NEXT:  START
+; DIFF-NEXT: -forbidden string
+; DIFF-NEXT: +This is a forbidden string
+; DIFF-NEXT:  END
+; DIFF-EMPTY:
+; DIFF-NEXT: FileCheck: Found 1 unique textual mismatch.



More information about the llvm-commits mailing list