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
