[clang-tools-extra] [clang-tidy] Add a new check 'performance-string-view-conversions' (PR #174288)
Victor Chernyakin via cfe-commits
cfe-commits at lists.llvm.org
Tue Jan 6 13:15:53 PST 2026
================
@@ -0,0 +1,144 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "StringViewConversionsCheck.h"
+#include "clang/AST/Expr.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Lex/Lexer.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::performance {
+
+static auto getStringTypeMatcher(StringRef CharType) {
+ return hasCanonicalType(hasDeclaration(cxxRecordDecl(hasName(CharType))));
+}
+
+void StringViewConversionsCheck::registerMatchers(MatchFinder *Finder) {
+ // Matchers for std::basic_string[_view] families
+ // (includes std::string, std::wstring, std::u8string, etc.)
+ const auto IsStdString = getStringTypeMatcher("::std::basic_string");
+ const auto IsStdStringView = getStringTypeMatcher("::std::basic_string_view");
+
+ // Matches pointer to any character type (char*, const char*, wchar_t*, etc.)
+ // or array of any character type (char[], char[N], etc.)
+ const auto IsCharPointerOrArray =
+ anyOf(hasType(pointerType(pointee(isAnyCharacter()))),
+ hasType(arrayType(hasElementType(isAnyCharacter()))));
+
+ // Matches expressions that can be implicitly converted to string_view:
+ // - string_view itself
+ // - string literals ("hello", L"hello", u8"hello", etc.)
+ // - character pointers (const char*, char*)
+ // - character arrays (char[], char[N])
+ const auto ImplicitlyConvertibleToStringView =
+ expr(anyOf(hasType(IsStdStringView), stringLiteral(),
+ IsCharPointerOrArray))
+ .bind("originalStringView");
+
+ // Matches std::string construction from an expression that is
+ // implicitly convertible to string_view.
+ // Excludes copy and move constructors to avoid false positives.
+ const auto RedundantStringConstruction = cxxConstructExpr(
+ hasType(IsStdString),
+ hasArgument(0, ignoringImplicit(ImplicitlyConvertibleToStringView)),
+ unless(hasDeclaration(cxxConstructorDecl(isCopyConstructor()))),
+ unless(hasDeclaration(cxxConstructorDecl(isMoveConstructor()))));
+
+ // Matches functional cast syntax: std::string(expr)
+ // This produces CXXFunctionalCastExpr in the AST.
+ const auto RedundantFunctionalCast =
+ cxxFunctionalCastExpr(hasType(IsStdString),
+ hasDescendant(RedundantStringConstruction))
+ .bind("redundantExpr");
+
+ // Matches brace initialization syntax: std::string{expr}
+ // This produces CXXTemporaryObjectExpr in the AST.
+ const auto RedundantTemporaryObject =
+ cxxTemporaryObjectExpr(
+ hasType(IsStdString),
+ hasArgument(0, ignoringImplicit(ImplicitlyConvertibleToStringView)),
+ unless(hasDeclaration(cxxConstructorDecl(isCopyConstructor()))),
+ unless(hasDeclaration(cxxConstructorDecl(isMoveConstructor()))))
+ .bind("redundantExpr");
+
+ // Main matcher: finds function calls where an argument:
+ // 1. Has type string_view (after implicit conversions)
+ // 2. Contains a redundant std::string construction (either syntax)
+ // 3. Does NOT contain operator+ (which requires std::string operands,
+ // so the conversion would not be redundant in that case)
+ //
+ // Example patterns detected:
+ // foo(std::string(sv)) - string_view -> string -> string_view
+ // foo(std::string{"lit"}) - literal -> string -> string_view
+ //
+ // Example patterns excluded:
+ // foo(std::string(sv) + "bar") - operator+ needs std::string
+ Finder->addMatcher(
+ callExpr(
+ forEachArgumentWithParam(
+ expr(hasType(IsStdStringView),
+ hasDescendant(expr(anyOf(RedundantFunctionalCast,
+ RedundantTemporaryObject))),
+ // Exclude cases where std::string is used with operator+
+ // since string_view doesn't support concatenation.
+ unless(hasDescendant(cxxOperatorCallExpr(
+ hasOverloadedOperatorName("+"), hasType(IsStdString)))))
+ .bind("expr"),
+ parmVarDecl(hasType(IsStdStringView))))
+ .bind("call"),
+ this);
+}
+
+void StringViewConversionsCheck::check(const MatchFinder::MatchResult &Result) {
+ // Get the full argument expression passed to the function.
+ // This has type string_view after implicit conversions.
+ const auto *ParamExpr = Result.Nodes.getNodeAs<Expr>("expr");
+ if (!ParamExpr)
+ return;
+
+ // Get the redundant std::string construction expression.
+ // This is either CXXFunctionalCastExpr for std::string(x) syntax
+ // or CXXTemporaryObjectExpr for std::string{x} syntax.
+ const auto *RedundantExpr = Result.Nodes.getNodeAs<Expr>("redundantExpr");
+ if (!RedundantExpr)
+ return;
+
+ // Get the original expression that was passed to std::string constructor.
+ // This is what we want to use as the replacement.
+ const auto *OriginalExpr = Result.Nodes.getNodeAs<Expr>("originalStringView");
+ if (!OriginalExpr)
+ return;
+
+ // Sanity check. Verify that the redundant expression is the direct source of
+ // the argument, not part of a larger expression (e.g., std::string(sv) +
+ // "bar"). If source ranges don't match, there's something between the string
+ // construction and the function argument, so we shouldn't transform.
+ if (ParamExpr->getSourceRange() != RedundantExpr->getSourceRange())
+ return;
----------------
localspook wrote:
If this isn't something we expect to ever happen, I think it makes more sense to `assert` it
https://github.com/llvm/llvm-project/pull/174288
More information about the cfe-commits
mailing list