[clang] 725fb38 - [SpecialCaseList] Add backward compatible dot-slash handling (#162511)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Jun 11 00:04:44 PDT 2026
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) {
More information about the cfe-commits
mailing list