https://github.com/hjanuschka updated https://github.com/llvm/llvm-project/pull/120055
>From 8b2dc9adf4fae2065823e5beb3a1cd851686913c Mon Sep 17 00:00:00 2001 From: Helmut Januschka <hel...@januschka.com> Date: Mon, 16 Dec 2024 08:24:14 +0100 Subject: [PATCH 1/2] [clang-tidy] Add readability-string-view-substr check Add a new check that suggests using string_view::remove_prefix() and remove_suffix() instead of substr() when the intent is to remove characters from either end of a string_view. --- .../clang-tidy/readability/CMakeLists.txt | 1 + .../readability/ReadabilityTidyModule.cpp | 3 + .../readability/StringViewSubstrCheck.cpp | 132 ++++++++++++++++++ .../readability/StringViewSubstrCheck.h | 39 ++++++ clang-tools-extra/docs/ReleaseNotes.rst | 7 + .../checks/readability/string-view-substr.rst | 16 +++ .../readability/stringview_substr.cpp | 55 ++++++++ 7 files changed, 253 insertions(+) create mode 100644 clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.cpp create mode 100644 clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.h create mode 100644 clang-tools-extra/docs/clang-tidy/checks/readability/string-view-substr.rst create mode 100644 clang-tools-extra/test/clang-tidy/checkers/readability/stringview_substr.cpp diff --git a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt index 8f303c51e1b0da..8b44fc339441ac 100644 --- a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt @@ -53,6 +53,7 @@ add_clang_library(clangTidyReadabilityModule STATIC StaticAccessedThroughInstanceCheck.cpp StaticDefinitionInAnonymousNamespaceCheck.cpp StringCompareCheck.cpp + StringViewSubstrCheck.cpp SuspiciousCallArgumentCheck.cpp UniqueptrDeleteReleaseCheck.cpp UppercaseLiteralSuffixCheck.cpp diff --git a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp index d61c0ba39658e5..f36ec8f95ede60 100644 --- a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp @@ -56,6 +56,7 @@ #include "StaticAccessedThroughInstanceCheck.h" #include "StaticDefinitionInAnonymousNamespaceCheck.h" #include "StringCompareCheck.h" +#include "StringViewSubstrCheck.h" #include "SuspiciousCallArgumentCheck.h" #include "UniqueptrDeleteReleaseCheck.h" #include "UppercaseLiteralSuffixCheck.h" @@ -146,6 +147,8 @@ class ReadabilityModule : public ClangTidyModule { "readability-static-definition-in-anonymous-namespace"); CheckFactories.registerCheck<StringCompareCheck>( "readability-string-compare"); + CheckFactories.registerCheck<StringViewSubstrCheck>( + "readability-stringview-substr"); CheckFactories.registerCheck<readability::NamedParameterCheck>( "readability-named-parameter"); CheckFactories.registerCheck<NonConstParameterCheck>( diff --git a/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.cpp b/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.cpp new file mode 100644 index 00000000000000..e86a971695a835 --- /dev/null +++ b/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.cpp @@ -0,0 +1,132 @@ +//===--- StringViewSubstrCheck.cpp - clang-tidy------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "StringViewSubstrCheck.h" +#include "clang/AST/ASTContext.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/Lex/Lexer.h" + +using namespace clang::ast_matchers; + +namespace clang::tidy::readability { + +void StringViewSubstrCheck::registerMatchers(MatchFinder *Finder) { + const auto HasStringViewType = hasType(hasUnqualifiedDesugaredType(recordType( + hasDeclaration(recordDecl(hasName("::std::basic_string_view")))))); + + // Match assignment to string_view's substr + Finder->addMatcher( + cxxOperatorCallExpr( + hasOverloadedOperatorName("="), + hasArgument(0, expr(HasStringViewType).bind("target")), + hasArgument( + 1, cxxMemberCallExpr(callee(memberExpr(hasDeclaration( + cxxMethodDecl(hasName("substr"))))), + on(expr(HasStringViewType).bind("source"))) + .bind("substr_call"))) + .bind("assignment"), + this); +} + +void StringViewSubstrCheck::check(const MatchFinder::MatchResult &Result) { + const auto *Assignment = + Result.Nodes.getNodeAs<CXXOperatorCallExpr>("assignment"); + const auto *Target = Result.Nodes.getNodeAs<Expr>("target"); + const auto *Source = Result.Nodes.getNodeAs<Expr>("source"); + const auto *SubstrCall = + Result.Nodes.getNodeAs<CXXMemberCallExpr>("substr_call"); + + if (!Assignment || !Target || !Source || !SubstrCall) { + return; + } + + // Get the DeclRefExpr for the target and source to compare variables + const auto *TargetDRE = dyn_cast<DeclRefExpr>(Target->IgnoreParenImpCasts()); + const auto *SourceDRE = dyn_cast<DeclRefExpr>(Source->IgnoreParenImpCasts()); + + // Only handle self-assignment cases + if (!TargetDRE || !SourceDRE || + TargetDRE->getDecl() != SourceDRE->getDecl()) { + return; + } + + const Expr *StartArg = SubstrCall->getArg(0); + const Expr *LengthArg = + SubstrCall->getNumArgs() > 1 ? SubstrCall->getArg(1) : nullptr; + + // Get source text of first argument + std::string StartText = + Lexer::getSourceText( + CharSourceRange::getTokenRange(StartArg->getSourceRange()), + *Result.SourceManager, Result.Context->getLangOpts()) + .str(); + + // Case 1: Check for remove_prefix pattern - only when the second arg is + // missing (uses npos) + if (!LengthArg || isa<CXXDefaultArgExpr>(LengthArg)) { + std::string Replacement = TargetDRE->getNameInfo().getAsString() + + ".remove_prefix(" + StartText + ")"; + diag(Assignment->getBeginLoc(), "prefer 'remove_prefix' over 'substr' for " + "removing characters from the start") + << FixItHint::CreateReplacement(Assignment->getSourceRange(), + Replacement); + return; + } + + // Case 2: Check for remove_suffix pattern + if (StartText == "0") { + if (const auto *BinOp = dyn_cast<BinaryOperator>(LengthArg)) { + if (BinOp->getOpcode() == BO_Sub) { + const Expr *LHS = BinOp->getLHS(); + const Expr *RHS = BinOp->getRHS(); + + // Check if LHS is a length() call on the same string_view + if (const auto *LengthCall = dyn_cast<CXXMemberCallExpr>(LHS)) { + if (const auto *LengthMethod = + dyn_cast<CXXMethodDecl>(LengthCall->getDirectCallee())) { + if (LengthMethod->getName() == "length") { + // Verify the length() call is on the same string_view + const Expr *LengthObject = + LengthCall->getImplicitObjectArgument(); + const auto *LengthDRE = + dyn_cast<DeclRefExpr>(LengthObject->IgnoreParenImpCasts()); + + if (!LengthDRE || LengthDRE->getDecl() != TargetDRE->getDecl()) { + return; + } + + // Must be a simple non-zero integer literal + const auto *IL = + dyn_cast<IntegerLiteral>(RHS->IgnoreParenImpCasts()); + if (!IL || IL->getValue() == 0) { + return; + } + + std::string RHSText = + Lexer::getSourceText( + CharSourceRange::getTokenRange(RHS->getSourceRange()), + *Result.SourceManager, Result.Context->getLangOpts()) + .str(); + + std::string Replacement = TargetDRE->getNameInfo().getAsString() + + ".remove_suffix(" + RHSText + ")"; + diag(Assignment->getBeginLoc(), + "prefer 'remove_suffix' over 'substr' for removing " + "characters from the end") + << FixItHint::CreateReplacement(Assignment->getSourceRange(), + Replacement); + return; + } + } + } + } + } + } +} + +} // namespace clang::tidy::readability diff --git a/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.h b/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.h new file mode 100644 index 00000000000000..1a2054da1ed9cc --- /dev/null +++ b/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.h @@ -0,0 +1,39 @@ +//===--- StringViewSubstrCheck.h - clang-tidy---------------------*- C++ +//-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_STRINGVIEWSUBSTRCHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_STRINGVIEWSUBSTRCHECK_H + +#include "../ClangTidyCheck.h" + +namespace clang::tidy::readability { + +/// Finds string_view substr() calls that can be replaced with remove_prefix() +/// or remove_suffix(). +/// +/// For the user-facing documentation see: +/// https://clang.llvm.org/extra/clang-tidy/checks/readability/string-view-substr.html +/// +/// The check matches two patterns: +/// sv = sv.substr(N) -> sv.remove_prefix(N) +/// sv = sv.substr(0, sv.length() - N) -> sv.remove_suffix(N) +/// +/// These replacements make the intent clearer and are more efficient as they +/// modify the string_view in place rather than creating a new one. +class StringViewSubstrCheck : public ClangTidyCheck { +public: + StringViewSubstrCheck(StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context) {} + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; +}; + +} // namespace clang::tidy::readability + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_STRINGVIEWSUBSTRCHECK_H \ No newline at end of file diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst index 453a91e3b504cd..4eb6c98bcf461b 100644 --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -136,6 +136,13 @@ New checks Gives warnings for tagged unions, where the number of tags is different from the number of data members inside the union. +- New :doc:`readability-string-view-substr + <clang-tidy/checks/readability/string-view-substr>` check. + + Finds ``std::string_view::substr()`` calls that can be replaced with clearer + alternatives using ``remove_prefix()`` or ``remove_suffix()``. This makes the + intent clearer and is more efficient as it modifies the string_view in place. + - New :doc:`portability-template-virtual-member-function <clang-tidy/checks/portability/template-virtual-member-function>` check. diff --git a/clang-tools-extra/docs/clang-tidy/checks/readability/string-view-substr.rst b/clang-tools-extra/docs/clang-tidy/checks/readability/string-view-substr.rst new file mode 100644 index 00000000000000..f29bcbacc0d7d1 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/readability/string-view-substr.rst @@ -0,0 +1,16 @@ +.. title:: clang-tidy - readability-string-view-substr + +readability-string-view-substr +============================== + +Finds ``string_view substr()`` calls that can be replaced with clearer alternatives +using ``remove_prefix()`` or ``remove_suffix()``. + +The check suggests the following transformations: + +=========================================== ======================= +Expression Replacement +=========================================== ======================= +``sv = sv.substr(n)`` ``sv.remove_prefix(n)`` +``sv = sv.substr(0, sv.length()-n)`` ``sv.remove_suffix(n)`` +=========================================== ======================= diff --git a/clang-tools-extra/test/clang-tidy/checkers/readability/stringview_substr.cpp b/clang-tools-extra/test/clang-tidy/checkers/readability/stringview_substr.cpp new file mode 100644 index 00000000000000..274c305b640803 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/readability/stringview_substr.cpp @@ -0,0 +1,55 @@ +// RUN: %check_clang_tidy %s readability-stringview-substr %t + +namespace std { +template <typename T> +class basic_string_view { +public: + using size_type = unsigned long; + static constexpr size_type npos = -1; + + basic_string_view(const char*) {} + basic_string_view substr(size_type pos, size_type count = npos) const { return *this; } + void remove_prefix(size_type n) {} + void remove_suffix(size_type n) {} + size_type length() const { return 0; } + + // Needed for assignment operator + basic_string_view& operator=(const basic_string_view&) { return *this; } +}; + +using string_view = basic_string_view<char>; +} + +void test() { + std::string_view sv("test"); + std::string_view sv1("test"); + std::string_view sv2("other"); + + // Should match: Removing N characters from start + sv = sv.substr(5); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_prefix' over 'substr' for removing characters from the start [readability-stringview-substr] + // CHECK-FIXES: sv.remove_prefix(5) + + // Should match: Removing N characters from end + sv = sv.substr(0, sv.length() - 3); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr] + // CHECK-FIXES: sv.remove_suffix(3) + + // Should not match: Basic substring operations + sv = sv.substr(0, 3); // No warning - taking first N characters + sv = sv.substr(1, 3); // No warning - taking N characters from position 1 + + // Should not match: Operations on different string_views + sv = sv2.substr(0, sv2.length() - 3); // No warning - not self-assignment + sv = sv.substr(0, sv2.length() - 3); // No warning - length() from different string_view + sv1 = sv1.substr(0, sv2.length() - 3); // No warning - length() from different string_view + sv = sv1.substr(0, sv1.length() - 3); // No warning - not self-assignment + + // Should not match: Not actually removing from end + sv = sv.substr(0, sv.length()); // No warning - keeping entire string + sv = sv.substr(0, sv.length() - 0); // No warning - subtracting zero + + // Should not match: Complex expressions + sv = sv.substr(0, sv.length() - (3 + 2)); // No warning - complex arithmetic + sv = sv.substr(1 + 2, sv.length() - 3); // No warning - complex start position +} >From 64931c696fe64cdb8a7571029eea0cc40592a663 Mon Sep 17 00:00:00 2001 From: Helmut Januschka <hel...@januschka.com> Date: Mon, 16 Dec 2024 11:14:12 +0100 Subject: [PATCH 2/2] up --- .../readability/StringViewSubstrCheck.cpp | 19 ++++++++- .../readability/stringview_substr.cpp | 41 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.cpp b/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.cpp index e86a971695a835..01c3895f61f5c4 100644 --- a/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.cpp +++ b/clang-tools-extra/clang-tidy/readability/StringViewSubstrCheck.cpp @@ -79,7 +79,24 @@ void StringViewSubstrCheck::check(const MatchFinder::MatchResult &Result) { } // Case 2: Check for remove_suffix pattern - if (StartText == "0") { + const Expr *Start = StartArg->IgnoreParenImpCasts(); + bool IsZero = false; + + if (const auto *IL = dyn_cast<IntegerLiteral>(Start)) { + IsZero = IL->getValue() == 0; + } else if (const auto *DRE = dyn_cast<DeclRefExpr>(Start)) { + // Check constants + if (const auto *VD = dyn_cast<VarDecl>(DRE->getDecl())) { + if (VD->hasInit()) { + if (const auto *Init = dyn_cast<IntegerLiteral>( + VD->getInit()->IgnoreParenImpCasts())) { + IsZero = Init->getValue() == 0; + } + } + } + } + + if (IsZero) { if (const auto *BinOp = dyn_cast<BinaryOperator>(LengthArg)) { if (BinOp->getOpcode() == BO_Sub) { const Expr *LHS = BinOp->getLHS(); diff --git a/clang-tools-extra/test/clang-tidy/checkers/readability/stringview_substr.cpp b/clang-tools-extra/test/clang-tidy/checkers/readability/stringview_substr.cpp index 274c305b640803..2c1d9df3711b1d 100644 --- a/clang-tools-extra/test/clang-tidy/checkers/readability/stringview_substr.cpp +++ b/clang-tools-extra/test/clang-tidy/checkers/readability/stringview_substr.cpp @@ -53,3 +53,44 @@ void test() { sv = sv.substr(0, sv.length() - (3 + 2)); // No warning - complex arithmetic sv = sv.substr(1 + 2, sv.length() - 3); // No warning - complex start position } + +void test_zeros() { + std::string_view sv("test"); + const int kZero = 0; + constexpr std::string_view::size_type Zero = 0; // Fixed: using string_view::size_type + #define START_POS 0 + + // All of these should match remove_suffix pattern and trigger warnings + sv = sv.substr(0, sv.length() - 3); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr] + // CHECK-FIXES: sv.remove_suffix(3) + + sv = sv.substr(kZero, sv.length() - 3); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr] + // CHECK-FIXES: sv.remove_suffix(3) + + sv = sv.substr(Zero, sv.length() - 3); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr] + // CHECK-FIXES: sv.remove_suffix(3) + + sv = sv.substr(START_POS, sv.length() - 3); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr] + // CHECK-FIXES: sv.remove_suffix(3) + + sv = sv.substr((0), sv.length() - 3); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr] + // CHECK-FIXES: sv.remove_suffix(3) + + sv = sv.substr(0u, sv.length() - 3); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr] + // CHECK-FIXES: sv.remove_suffix(3) + + sv = sv.substr(0UL, sv.length() - 3); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: prefer 'remove_suffix' over 'substr' for removing characters from the end [readability-stringview-substr] + // CHECK-FIXES: sv.remove_suffix(3) + + // These should NOT match the remove_suffix pattern + sv = sv.substr(1-1, sv.length() - 3); // No warning - complex expression + sv = sv.substr(sv.length() - 3, sv.length() - 3); // No warning - non-zero start +} + _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits