Author: Vitaly Buka
Date: 2026-06-11T07:04:37Z
New Revision: 725fb3845d2df3267983590e2228569126468c96

URL: 
https://github.com/llvm/llvm-project/commit/725fb3845d2df3267983590e2228569126468c96
DIFF: 
https://github.com/llvm/llvm-project/commit/725fb3845d2df3267983590e2228569126468c96.diff

LOG: [SpecialCaseList] Add backward compatible dot-slash handling (#162511)

This PR is preparation for:
* https://github.com/llvm/llvm-project/pull/167283

The new behavior is controlled by the `Version` field in the special
case list file.

- Version 1 and 2: Path is matched as-is, regardless of presence of
"./".
- Version 3, 5 and higher: Paths with leading dot-slash are
canonicalized
  to paths without dot-slash before matching. This means that a rule
  like `src=./foo` will never match, and `src=foo` will match both
`foo` and `./foo`. (Version 3 never became default but has this
behavior).
- Version 4: Transitionary version. Paths are matched both ways
(canonicalized and non-canonicalized) to maintain backward
compatibility.
If a match only works with the old behavior (non-canonicalized), a
warning
  is emitted.

This change allows for a gradual transition to the new behavior, while
maintaining backward compatibility with existing special case list
files.

Added: 
    

Modified: 
    clang/docs/ReleaseNotes.rst
    clang/docs/SanitizerSpecialCaseList.rst
    llvm/lib/Support/SpecialCaseList.cpp
    llvm/unittests/Support/SpecialCaseListTest.cpp

Removed: 
    


################################################################################
diff  --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 5e7a0c76d4594..a0a1daeb3caa2 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -1009,6 +1009,14 @@ Sanitizers
 ----------
 - UndefinedBehaviorSanitizer now supports ``__ubsan_default_suppressions``.
 
+- Sanitizer Special Case Lists (``-fsanitize-ignorelist``) now support
+  Version 4 of the Special Case List format, which introduces a transition
+  period for leading dot-slash (``./``) canonicalization in path matching.
+  Version 4 matches both canonicalized and non-canonicalized paths but emits a
+  warning for deprecated matches. Version 5 drops backward compatibility and
+  requires rules to match canonicalized paths (without leading ``./``).
+
+
 Python Binding Changes
 ----------------------
 - Add deprecation warnings to ``CompletionChunk.isKind...`` methods.

diff  --git a/clang/docs/SanitizerSpecialCaseList.rst 
b/clang/docs/SanitizerSpecialCaseList.rst
index a2d942154a830..1de3555c5a8ce 100644
--- a/clang/docs/SanitizerSpecialCaseList.rst
+++ b/clang/docs/SanitizerSpecialCaseList.rst
@@ -230,6 +230,27 @@ tool-specific docs.
     [{cfi-vcall,cfi-icall}]
     fun:*BadCfiCall
 
+.. note::
+
+  By default, path matching (for ``src`` and ``mainfile``) matches the query
+  path as-is. For example, a query with ``./foo.c`` will not match a rule
+  defined as ``src:foo.c``.
+
+  Starting with version 3 (indicated by ``#!special-case-list-v3``), leading
+  ``./`` is canonicalized (removed) from paths before matching. This means
+  a rule like ``src:foo.c`` will match both ``foo.c`` and ``./foo.c``, while
+  a rule like ``src:./foo.c`` will no longer match.
+
+  Version 4 (indicated by ``#!special-case-list-v4``) is a transition version
+  that maintains backward compatibility by matching both canonicalized and
+  non-canonicalized paths, but emits a warning if a match would be lost in
+  Version 5 (i.e., if it only matches because of the deprecated leading ``./``
+  in the rule).
+
+  Version 5 (indicated by ``#!special-case-list-v5``) drops backward
+  compatibility and behaves like Version 3.
+
+
 ``mainfile`` is similar to applying ``-fno-sanitize=`` to a set of files but
 does not need plumbing into the build system. This works well for internal
 linkage functions but has a caveat for C++ vague linkage functions.

diff  --git a/llvm/lib/Support/SpecialCaseList.cpp 
b/llvm/lib/Support/SpecialCaseList.cpp
index a025772afdfb2..d72f7e7fd1d81 100644
--- a/llvm/lib/Support/SpecialCaseList.cpp
+++ b/llvm/lib/Support/SpecialCaseList.cpp
@@ -26,8 +26,10 @@
 #include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/Regex.h"
 #include "llvm/Support/VirtualFileSystem.h"
-#include "llvm/Support/raw_ostream.h"
+#include "llvm/Support/WithColor.h"
+#include <assert.h>
 #include <memory>
+#include <mutex>
 #include <stdio.h>
 #include <string>
 #include <system_error>
@@ -92,6 +94,7 @@ class GlobMatcher {
 struct QueryOptions {
   bool UseGlobs = true;
   bool RemoveDotSlash = false;
+  bool WarnDotSlashMatch = false;
 };
 
 /// Represents a set of patterns and their line numbers
@@ -110,6 +113,7 @@ class Matcher {
 
   std::variant<RegexMatcher, GlobMatcher> M;
   QueryOptions Options;
+  mutable std::once_flag Warned;
 };
 
 Error RegexMatcher::insert(StringRef Pattern, unsigned LineNumber) {
@@ -256,10 +260,40 @@ Error Matcher::insert(StringRef Pattern, unsigned 
LineNumber) {
   return std::visit([&](auto &V) { return V.insert(Pattern, LineNumber); }, M);
 }
 
+/// Matches Query against the patterns. The behavior is controlled by
+/// `#!special-case-list` version.
+//
+// - Version 1 and 2: Path is matched as-is, regardless of presence of "./".
+// - Version 3, 5 and higher: Paths with leading dot-slash are canonicalized
+//   to paths without dot-slash before matching. This means that a rule
+//   like `src=./foo` will never match, and `src=foo` will match both
+//   `foo` and `./foo`. (Version 3 never became default but has this behavior).
+// - Version 4: Transitionary version. Paths are matched both ways
+//   (canonicalized and non-canonicalized) to maintain backward compatibility.
+//   If a match only works with the old behavior (non-canonicalized), a warning
+//   is emitted.
 unsigned Matcher::match(StringRef Query) const {
-  if (Options.RemoveDotSlash)
-    Query = llvm::sys::path::remove_leading_dotslash(Query);
-  return matchInternal(Query);
+  if (!Options.RemoveDotSlash)
+    return matchInternal(Query);
+
+  if (!Options.WarnDotSlashMatch)
+    return matchInternal(llvm::sys::path::remove_leading_dotslash(Query));
+
+  StringRef FixedQuery = llvm::sys::path::remove_leading_dotslash(Query);
+  unsigned FixedMatched = matchInternal(FixedQuery);
+  if (FixedQuery == Query)
+    return FixedMatched;
+
+  unsigned OriginalMatch = matchInternal(Query);
+  if (OriginalMatch > FixedMatched) {
+    std::call_once(Warned, [&]() {
+      WithColor::warning() << "Deprecated behaviour: pattern '"
+                           << findRule(OriginalMatch) << "' matches '" << Query
+                           << "', update it to match '" << FixedQuery
+                           << "' instead (further warnings suppressed).\n";
+    });
+  }
+  return std::max(OriginalMatch, FixedMatched);
 }
 
 unsigned Matcher::matchInternal(StringRef Query) const {
@@ -370,8 +404,8 @@ bool SpecialCaseList::parse(unsigned FileIdx, const 
MemoryBuffer *MB,
   // first line of the file. For more details, see
   // 
https://discourse.llvm.org/t/use-glob-instead-of-regex-for-specialcaselists/71666
   bool UseGlobs = MinVersion(2);
-
   bool RemoveDotSlash = MinVersion(3);
+  bool WarnDotSlash = MinVersion(4) && !MinVersion(5);
 
   auto ErrOrSection = addSection("*", FileIdx, 1, true);
   if (auto Err = ErrOrSection.takeError()) {
@@ -420,8 +454,10 @@ bool SpecialCaseList::parse(unsigned FileIdx, const 
MemoryBuffer *MB,
 
     QueryOptions QOpts;
     QOpts.UseGlobs = UseGlobs;
-    if (llvm::is_contained(PathPrefixes, Prefix))
+    if (llvm::is_contained(PathPrefixes, Prefix)) {
       QOpts.RemoveDotSlash = RemoveDotSlash;
+      QOpts.WarnDotSlashMatch = WarnDotSlash;
+    }
 
     auto [Pattern, Category] = Postfix.split("=");
     auto [It, _] = CurrentImpl->Entries[Prefix].try_emplace(Category, QOpts);

diff  --git a/llvm/unittests/Support/SpecialCaseListTest.cpp 
b/llvm/unittests/Support/SpecialCaseListTest.cpp
index 812e0d3d8520c..5bcd111f53059 100644
--- a/llvm/unittests/Support/SpecialCaseListTest.cpp
+++ b/llvm/unittests/Support/SpecialCaseListTest.cpp
@@ -319,38 +319,86 @@ TEST_F(SpecialCaseListTest, DotSlash) {
       makeSpecialCaseList(IgnoreList, /*Version=*/3);
   std::unique_ptr<SpecialCaseList> SCL4 = makeSpecialCaseList(IgnoreList,
                                                               /*Version=*/4);
+  std::unique_ptr<SpecialCaseList> SCL5 = makeSpecialCaseList(IgnoreList,
+                                                              /*Version=*/5);
 
   EXPECT_TRUE(SCL2->inSection("dot", "fun", "./foo"));
   EXPECT_TRUE(SCL3->inSection("dot", "fun", "./foo"));
   EXPECT_TRUE(SCL4->inSection("dot", "fun", "./foo"));
+  EXPECT_TRUE(SCL5->inSection("dot", "fun", "./foo"));
 
   EXPECT_FALSE(SCL2->inSection("dot", "fun", "foo"));
   EXPECT_FALSE(SCL3->inSection("dot", "fun", "foo"));
   EXPECT_FALSE(SCL4->inSection("dot", "fun", "foo"));
+  EXPECT_FALSE(SCL5->inSection("dot", "fun", "foo"));
 
   EXPECT_TRUE(SCL2->inSection("dot", "src", "./bar"));
   EXPECT_FALSE(SCL3->inSection("dot", "src", "./bar"));
-  EXPECT_FALSE(SCL4->inSection("dot", "src", "./bar"));
+  EXPECT_TRUE(SCL4->inSection("dot", "src", "./bar"));
+  EXPECT_FALSE(SCL5->inSection("dot", "src", "./bar"));
 
   EXPECT_FALSE(SCL2->inSection("dot", "src", "bar"));
   EXPECT_FALSE(SCL3->inSection("dot", "src", "bar"));
   EXPECT_FALSE(SCL4->inSection("dot", "src", "bar"));
+  EXPECT_FALSE(SCL5->inSection("dot", "src", "bar"));
 
   EXPECT_FALSE(SCL2->inSection("not", "fun", "./foo"));
   EXPECT_FALSE(SCL3->inSection("not", "fun", "./foo"));
   EXPECT_FALSE(SCL4->inSection("not", "fun", "./foo"));
+  EXPECT_FALSE(SCL5->inSection("not", "fun", "./foo"));
 
   EXPECT_TRUE(SCL2->inSection("not", "fun", "foo"));
   EXPECT_TRUE(SCL3->inSection("not", "fun", "foo"));
   EXPECT_TRUE(SCL4->inSection("not", "fun", "foo"));
+  EXPECT_TRUE(SCL5->inSection("not", "fun", "foo"));
 
   EXPECT_FALSE(SCL2->inSection("not", "src", "./bar"));
   EXPECT_TRUE(SCL3->inSection("not", "src", "./bar"));
   EXPECT_TRUE(SCL4->inSection("not", "src", "./bar"));
+  EXPECT_TRUE(SCL5->inSection("not", "src", "./bar"));
 
   EXPECT_TRUE(SCL2->inSection("not", "src", "bar"));
   EXPECT_TRUE(SCL3->inSection("not", "src", "bar"));
   EXPECT_TRUE(SCL4->inSection("not", "src", "bar"));
+  EXPECT_TRUE(SCL5->inSection("not", "src", "bar"));
+}
+
+TEST_F(SpecialCaseListTest, DotSlashWarning) {
+  StringRef IgnoreList = "[dot]\n"
+                         "src:./bar\n"
+                         "[not]\n"
+                         "src:bar\n";
+  std::unique_ptr<SpecialCaseList> SCL3 =
+      makeSpecialCaseList(IgnoreList, /*Version=*/3);
+  std::unique_ptr<SpecialCaseList> SCL4 =
+      makeSpecialCaseList(IgnoreList, /*Version=*/4);
+  std::unique_ptr<SpecialCaseList> SCL5 =
+      makeSpecialCaseList(IgnoreList, /*Version=*/5);
+
+  // Version 3 should not warn (new behavior, no warn)
+  testing::internal::CaptureStderr();
+  EXPECT_FALSE(SCL3->inSection("dot", "src", "./bar"));
+  EXPECT_TRUE(testing::internal::GetCapturedStderr().empty());
+
+  // Version 4 should warn (transition)
+  testing::internal::CaptureStderr();
+  EXPECT_TRUE(SCL4->inSection("dot", "src", "./bar"));
+  std::string Warning = testing::internal::GetCapturedStderr();
+  EXPECT_THAT(
+      Warning,
+      HasSubstr(
+          "warning: Deprecated behaviour: pattern './bar' matches './bar'"));
+  EXPECT_THAT(Warning, HasSubstr("update it to match 'bar' instead"));
+
+  // Version 5 should not warn (new behavior, no warn)
+  testing::internal::CaptureStderr();
+  EXPECT_FALSE(SCL5->inSection("dot", "src", "./bar"));
+  EXPECT_TRUE(testing::internal::GetCapturedStderr().empty());
+
+  // Version 4 should not warn here because it is a new match
+  testing::internal::CaptureStderr();
+  EXPECT_TRUE(SCL4->inSection("not", "src", "./bar"));
+  EXPECT_TRUE(testing::internal::GetCapturedStderr().empty());
 }
 
 TEST_F(SpecialCaseListTest, LinesInSection) {


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

Reply via email to