[clang-tools-extra] [clang-tidy] Add modernize-use-from-range-container-constructor check (PR #180868)
Victor Chernyakin via cfe-commits
cfe-commits at lists.llvm.org
Wed Feb 11 08:52:09 PST 2026
================
@@ -0,0 +1,237 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "UseFromRangeContainerConstructorCheck.h"
+
+#include <optional>
+#include <string>
+
+#include "../ClangTidyCheck.h"
+#include "../ClangTidyDiagnosticConsumer.h"
+#include "../ClangTidyOptions.h"
+#include "../utils/ASTUtils.h"
+#include "../utils/IncludeSorter.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/Type.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Basic/Diagnostic.h"
+#include "clang/Basic/LLVM.h"
+#include "clang/Basic/OperatorKinds.h"
+#include "clang/Basic/SourceLocation.h"
+#include "clang/Lex/Lexer.h"
+#include "clang/Tooling/FixIt.h"
+
+namespace clang::tidy::modernize {
+
+namespace {
+
+using ast_matchers::argumentCountAtLeast;
+using ast_matchers::cxxConstructExpr;
+using ast_matchers::cxxConstructorDecl;
+using ast_matchers::cxxRecordDecl;
+using ast_matchers::hasAnyName;
+using ast_matchers::hasDeclaration;
+using ast_matchers::ofClass;
+
+struct RangeObjectInfo {
+ const Expr *Object;
+ bool IsArrow;
+ StringRef Name;
+};
+
+} // namespace
+
+static std::optional<RangeObjectInfo> getRangeAndFunctionName(const Expr *E) {
+ E = E->IgnoreParenImpCasts();
+ const Expr *Base = nullptr;
+ bool IsArrow = false;
+ StringRef Name;
+ if (const auto *MemberCall = dyn_cast<CXXMemberCallExpr>(E)) {
+ if (const auto *ME = dyn_cast<MemberExpr>(
+ MemberCall->getCallee()->IgnoreParenImpCasts())) {
+ Base = ME->getBase()->IgnoreParenImpCasts();
+ IsArrow = ME->isArrow();
+ Name = ME->getMemberDecl()->getName();
+ }
+ } else if (const auto *Call = dyn_cast<CallExpr>(E)) {
+ if (Call->getNumArgs() == 1 && Call->getDirectCallee()) {
+ Base = Call->getArg(0)->IgnoreParenImpCasts();
+ IsArrow = false;
+ Name = Call->getDirectCallee()->getName();
+ }
+ }
+
+ if (!Base)
+ return std::nullopt;
+
+ // PEEL LAYER: Handle Smart Pointers (overloaded operator->)
+ // If the base is an operator call, we want the text of the underlying
+ // pointer.
+ if (const auto *OpCall = dyn_cast<CXXOperatorCallExpr>(Base)) {
+ if (OpCall->getOperator() == OO_Arrow) {
+ Base = OpCall->getArg(0)->IgnoreParenImpCasts();
+ IsArrow = true;
+ }
+ }
+
+ return RangeObjectInfo{Base, IsArrow, Name};
+}
+
+static QualType getValueType(QualType T) {
+ if (const auto *Spec = T->getAs<TemplateSpecializationType>()) {
+ const StringRef Name =
+ Spec->getTemplateName().getAsTemplateDecl()->getName();
+ if (Name == "map" || Name == "unordered_map")
+ return {};
+
+ if (Name == "unique_ptr") {
+ if (!Spec->template_arguments().empty() &&
+ Spec->template_arguments()[0].getKind() == TemplateArgument::Type)
+ return getValueType(Spec->template_arguments()[0].getAsType());
+ return {};
+ }
+
+ const ArrayRef<TemplateArgument> &Args = Spec->template_arguments();
+ if (!Args.empty() && Args[0].getKind() == TemplateArgument::Type)
+ return Args[0].getAsType();
+ }
+ return {};
+}
+
+UseFromRangeContainerConstructorCheck::UseFromRangeContainerConstructorCheck(
+ StringRef Name, ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context),
+ Inserter(Options.getLocalOrGlobal("IncludeStyle",
+ utils::IncludeSorter::IS_LLVM),
+ /*SelfContainedDiags=*/false) {}
+
+void UseFromRangeContainerConstructorCheck::registerPPCallbacks(
+ const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
+ Inserter.registerPreprocessor(PP);
+}
+
+void UseFromRangeContainerConstructorCheck::registerMatchers(
+ ast_matchers::MatchFinder *Finder) {
+ auto ContainerNames =
+ hasAnyName("::std::vector", "::std::deque", "::std::forward_list",
+ "::std::list", "::std::set", "::std::map",
+ "::std::unordered_set", "::std::unordered_map",
+ "::std::priority_queue", "::std::queue", "::std::stack",
+ "::std::basic_string", "::std::flat_set", "::std::flat_map");
+ Finder->addMatcher(cxxConstructExpr(argumentCountAtLeast(2),
+ hasDeclaration(cxxConstructorDecl(ofClass(
+ cxxRecordDecl(ContainerNames)))))
+ .bind("ctor"),
+ this);
+}
+
+void UseFromRangeContainerConstructorCheck::check(
+ const ast_matchers::MatchFinder::MatchResult &Result) {
+ const auto *CtorExpr = Result.Nodes.getNodeAs<CXXConstructExpr>("ctor");
+ std::optional<RangeObjectInfo> BeginInfo =
+ getRangeAndFunctionName(CtorExpr->getArg(0));
+ std::optional<RangeObjectInfo> EndInfo =
+ getRangeAndFunctionName(CtorExpr->getArg(1));
+ if (!BeginInfo || !EndInfo)
+ return;
+
+ if (!((BeginInfo->Name == "begin" && EndInfo->Name == "end") ||
+ (BeginInfo->Name == "cbegin" && EndInfo->Name == "cend"))) {
+ return;
+ }
+
+ if (!utils::areStatementsIdentical(BeginInfo->Object, EndInfo->Object,
+ *Result.Context)) {
+ return;
+ }
+
+ // Type compatibility check.
+ //
+ // 1) Same type, std::from_range works, warn.
+ //
+ // std::set<std::string> source;
+ // std::vector<std::string> dest(source.begin(), source.end());
+ //
+ // 2) Needs explicit conversion, std::from_range doesn't work, so don't warn.
+ //
+ // std::set<std::string_view> source;
+ // std::vector<std::string> dest(source.begin(), source.end());
+ //
+ // 3) Implicitly convertible, std::from_range works, but do not warn, since
+ // checking this case is hard in clang-tidy.
+ //
+ // std::set<std::string> source;
+ // std::vector<std::string_view> dest(source.begin(), source.end());
+ QualType SourceRangeType = BeginInfo->Object->getType();
+ if (const auto *Type = SourceRangeType->getAs<PointerType>())
+ SourceRangeType = Type->getPointeeType();
+ const QualType SourceValueType = getValueType(SourceRangeType);
+
+ if (const auto *DestSpec =
+ CtorExpr->getType()->getAs<TemplateSpecializationType>()) {
+ const StringRef Name =
+ DestSpec->getTemplateName().getAsTemplateDecl()->getName();
+ if ((Name == "map" || Name == "unordered_map") &&
+ !SourceValueType.isNull()) {
+ if (const auto *SourcePairSpec =
+ SourceValueType->getAs<TemplateSpecializationType>()) {
+ if (SourcePairSpec->getTemplateName().getAsTemplateDecl()->getName() ==
+ "pair") {
+ const QualType DestKeyType =
+ DestSpec->template_arguments()[0].getAsType();
+ const QualType SourceKeyType =
+ SourcePairSpec->template_arguments()[0].getAsType();
+ if (!ASTContext::hasSameUnqualifiedType(DestKeyType, SourceKeyType))
+ return;
+ }
+ }
+ }
+ }
+
+ const QualType DestValueType = getValueType(CtorExpr->getType());
+ if (!DestValueType.isNull() && !SourceValueType.isNull() &&
+ !ASTContext::hasSameUnqualifiedType(DestValueType, SourceValueType)) {
+ return;
+ }
+
+ std::string BaseText =
+ tooling::fixit::getText(*BeginInfo->Object, *Result.Context).str();
+ if (BaseText.empty())
+ return;
+
+ StringRef BaseRef(BaseText);
+ BaseRef.consume_back("->");
+ BaseText = BaseRef.str();
+ std::string Replacement = "std::from_range, ";
+ if (BeginInfo->IsArrow) {
+ // Determine if we need safety parentheses: *(p + 1) vs *p
+ const bool SimpleIdentifier =
+ BaseText.find_first_of(" +-*/%&|^") == std::string::npos;
+ Replacement += SimpleIdentifier ? "*" + BaseText : "*(" + BaseText + ")";
+ } else {
+ Replacement += BaseText;
+ }
+
+ const DiagnosticBuilder Diag =
+ diag(CtorExpr->getBeginLoc(),
+ "use std::from_range for container construction");
+ const SourceRange ArgRange(CtorExpr->getArg(0)->getBeginLoc(),
+ CtorExpr->getArg(1)->getEndLoc());
+ Diag << FixItHint::CreateReplacement(ArgRange, Replacement);
+ Diag << Inserter.createMainFileIncludeInsertion("<ranges>");
----------------
localspook wrote:
Adding this include is unnecessary; if `<vector>` has been included, you can use `std::vector`'s range constructor. And `<ranges>` is massive and a hit to compile time, so we really don't want to bring it in if we can help it (standard libraries go to pains to make sure that including `<vector>` doesn't transitively bring in all of `<ranges>`).
I believe everything to do with `IncludeInserter` could be removed from this check.
https://github.com/llvm/llvm-project/pull/180868
More information about the cfe-commits
mailing list