[clang-tools-extra] [clang-tidy] Add readability-use-span-first-last check (PR #118074)
Helmut Januschka via cfe-commits
cfe-commits at lists.llvm.org
Wed Feb 18 00:20:38 PST 2026
https://github.com/hjanuschka updated https://github.com/llvm/llvm-project/pull/118074
>From 35e04fe6516af26106195cc58b4f208ae6f36b74 Mon Sep 17 00:00:00 2001
From: Helmut Januschka <helmut at januschka.com>
Date: Wed, 18 Feb 2026 08:58:31 +0100
Subject: [PATCH] [clang-tidy] Add readability-use-span-first-last check
Add new clang-tidy check that suggests using std::span's more expressive
first() and last() member functions instead of equivalent subspan() calls.
These dedicated methods were added to C++20 to provide clearer alternatives
to common subspan operations. They improve readability by better expressing
intent and are less error-prone by eliminating manual offset calculations.
For example:
s.subspan(0, n) -> s.first(n)
s.subspan(s.size() - n) -> s.last(n)
Non-zero offset with count (like subspan(1, n)) or offset-only calls
(like subspan(n)) are not transformed as they have no clearer equivalent.
---
.../clang-tidy/readability/CMakeLists.txt | 1 +
.../readability/ReadabilityTidyModule.cpp | 3 +
.../readability/UseSpanFirstLastCheck.cpp | 126 ++++++++++++
.../readability/UseSpanFirstLastCheck.h | 43 ++++
clang-tools-extra/docs/ReleaseNotes.rst | 6 +
.../docs/clang-tidy/checks/list.rst | 1 +
.../readability/use-span-first-last.rst | 24 +++
.../readability/use-span-first-last.cpp | 186 ++++++++++++++++++
8 files changed, 390 insertions(+)
create mode 100644 clang-tools-extra/clang-tidy/readability/UseSpanFirstLastCheck.cpp
create mode 100644 clang-tools-extra/clang-tidy/readability/UseSpanFirstLastCheck.h
create mode 100644 clang-tools-extra/docs/clang-tidy/checks/readability/use-span-first-last.rst
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/readability/use-span-first-last.cpp
diff --git a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
index f1f3cde32feff..941416160cb21 100644
--- a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
@@ -63,6 +63,7 @@ add_clang_library(clangTidyReadabilityModule STATIC
UppercaseLiteralSuffixCheck.cpp
UseAnyOfAllOfCheck.cpp
UseConcisePreprocessorDirectivesCheck.cpp
+ UseSpanFirstLastCheck.cpp
UseStdMinMaxCheck.cpp
LINK_LIBS
diff --git a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
index c582dc98eac6b..f213c02dd9fee 100644
--- a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
@@ -65,6 +65,7 @@
#include "UppercaseLiteralSuffixCheck.h"
#include "UseAnyOfAllOfCheck.h"
#include "UseConcisePreprocessorDirectivesCheck.h"
+#include "UseSpanFirstLastCheck.h"
#include "UseStdMinMaxCheck.h"
namespace clang::tidy {
@@ -188,6 +189,8 @@ class ReadabilityModule : public ClangTidyModule {
"readability-use-anyofallof");
CheckFactories.registerCheck<UseConcisePreprocessorDirectivesCheck>(
"readability-use-concise-preprocessor-directives");
+ CheckFactories.registerCheck<UseSpanFirstLastCheck>(
+ "readability-use-span-first-last");
CheckFactories.registerCheck<UseStdMinMaxCheck>(
"readability-use-std-min-max");
}
diff --git a/clang-tools-extra/clang-tidy/readability/UseSpanFirstLastCheck.cpp b/clang-tools-extra/clang-tidy/readability/UseSpanFirstLastCheck.cpp
new file mode 100644
index 0000000000000..0e3a35dcf0ab0
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/readability/UseSpanFirstLastCheck.cpp
@@ -0,0 +1,126 @@
+//===--- UseSpanFirstLastCheck.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 "UseSpanFirstLastCheck.h"
+#include "../utils/Matchers.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Lex/Lexer.h"
+
+using namespace clang::ast_matchers;
+using namespace clang::tidy::matchers;
+
+namespace clang::tidy::readability {
+
+void UseSpanFirstLastCheck::registerMatchers(MatchFinder *Finder) {
+ // Type matcher for concrete std::span types.
+ const auto HasSpanType =
+ hasType(hasUnqualifiedDesugaredType(recordType(hasDeclaration(
+ classTemplateSpecializationDecl(hasName("::std::span"))))));
+
+ // Type matcher for dependent std::span types (in templates).
+ const auto HasDependentSpanType =
+ hasType(hasCanonicalType(hasDeclaration(namedDecl(hasName("::std::span")))));
+
+ // --- Non-dependent matchers (concrete types) ---
+
+ // Match span.subspan(0, n) -> first(n)
+ Finder->addMatcher(
+ cxxMemberCallExpr(
+ argumentCountIs(2),
+ callee(memberExpr(hasDeclaration(cxxMethodDecl(hasName("subspan"))))),
+ on(expr(HasSpanType).bind("span_object")),
+ hasArgument(0, integerLiteral(equals(0))),
+ hasArgument(1, expr().bind("count")))
+ .bind("first_subspan"),
+ this);
+
+ // Match span.subspan(span.size() - n) -> last(n)
+ const auto SizeCall = anyOf(
+ cxxMemberCallExpr(
+ callee(memberExpr(hasDeclaration(cxxMethodDecl(hasName("size"))))),
+ on(expr(isStatementIdenticalToBoundNode("span_object")))),
+ callExpr(callee(functionDecl(
+ hasAnyName("::std::size", "::std::ranges::size"))),
+ hasArgument(
+ 0, expr(isStatementIdenticalToBoundNode("span_object")))));
+
+ Finder->addMatcher(
+ cxxMemberCallExpr(
+ argumentCountIs(1),
+ callee(memberExpr(hasDeclaration(cxxMethodDecl(hasName("subspan"))))),
+ on(expr(HasSpanType).bind("span_object")),
+ hasArgument(0, binaryOperator(hasOperatorName("-"), hasLHS(SizeCall),
+ hasRHS(expr().bind("count")))))
+ .bind("last_subspan"),
+ this);
+
+ // --- Dependent matchers (template definitions) ---
+
+ const auto DependentSubspanCallee = callee(cxxDependentScopeMemberExpr(
+ hasMemberName("subspan"),
+ hasObjectExpression(
+ expr(anyOf(HasDependentSpanType, HasSpanType)).bind("span_object"))));
+
+ // Match span.subspan(0, n) -> first(n) in dependent context
+ Finder->addMatcher(
+ callExpr(argumentCountIs(2), DependentSubspanCallee,
+ hasArgument(0, integerLiteral(equals(0))),
+ hasArgument(1, expr().bind("count")))
+ .bind("first_subspan"),
+ this);
+
+ // Match span.subspan(span.size() - n) -> last(n) in dependent context
+ const auto DependentSizeCall = callExpr(callee(cxxDependentScopeMemberExpr(
+ hasMemberName("size"),
+ hasObjectExpression(
+ expr(isStatementIdenticalToBoundNode("span_object"))))));
+
+ Finder->addMatcher(
+ callExpr(argumentCountIs(1), DependentSubspanCallee,
+ hasArgument(0, binaryOperator(hasOperatorName("-"),
+ hasLHS(DependentSizeCall),
+ hasRHS(expr().bind("count")))))
+ .bind("last_subspan"),
+ this);
+}
+
+void UseSpanFirstLastCheck::check(const MatchFinder::MatchResult &Result) {
+ const auto *SpanObj = Result.Nodes.getNodeAs<Expr>("span_object");
+ if (!SpanObj)
+ return;
+
+ const auto *SubSpan = Result.Nodes.getNodeAs<CallExpr>("first_subspan");
+ bool IsFirst = true;
+ if (!SubSpan) {
+ SubSpan = Result.Nodes.getNodeAs<CallExpr>("last_subspan");
+ IsFirst = false;
+ }
+
+ if (!SubSpan)
+ return;
+
+ const auto *Count = Result.Nodes.getNodeAs<Expr>("count");
+ assert(Count && "Count expression must exist due to AST matcher");
+
+ const StringRef CountText = Lexer::getSourceText(
+ CharSourceRange::getTokenRange(Count->getSourceRange()),
+ *Result.SourceManager, Result.Context->getLangOpts());
+ const StringRef SpanText = Lexer::getSourceText(
+ CharSourceRange::getTokenRange(SpanObj->getSourceRange()),
+ *Result.SourceManager, Result.Context->getLangOpts());
+ const StringRef FirstOrLast = IsFirst ? "first" : "last";
+ const std::string Replacement =
+ (Twine(SpanText) + "." + FirstOrLast + "(" + CountText + ")").str();
+
+ diag(SubSpan->getBeginLoc(), "prefer 'span::%0()' over 'subspan()'")
+ << FirstOrLast
+ << FixItHint::CreateReplacement(SubSpan->getSourceRange(), Replacement);
+}
+} // namespace clang::tidy::readability
diff --git a/clang-tools-extra/clang-tidy/readability/UseSpanFirstLastCheck.h b/clang-tools-extra/clang-tidy/readability/UseSpanFirstLastCheck.h
new file mode 100644
index 0000000000000..69036df9f0706
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/readability/UseSpanFirstLastCheck.h
@@ -0,0 +1,43 @@
+//===--- UseSpanFirstLastCheck.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_USESPANFIRSTLASTCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_USESPANFIRSTLASTCHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::readability {
+
+/// Suggests using clearer 'std::span' member functions 'first()'/'last()'
+/// instead of equivalent 'subspan()' calls where applicable.
+///
+/// For example:
+/// \code
+/// std::span<int> s = ...;
+/// auto sub1 = s.subspan(0, n); // -> auto sub1 = s.first(n);
+/// auto sub2 = s.subspan(s.size() - n); // -> auto sub2 = s.last(n);
+/// auto sub3 = s.subspan(1, n); // not changed
+/// auto sub4 = s.subspan(n); // not changed
+/// \endcode
+///
+/// The check is only active in C++20 mode.
+class UseSpanFirstLastCheck : public ClangTidyCheck {
+public:
+ UseSpanFirstLastCheck(StringRef Name, ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context) {}
+
+ void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+ void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+ bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+ return LangOpts.CPlusPlus20;
+ }
+};
+
+} // namespace clang::tidy::readability
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_USESPANFIRSTLASTCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 2bb4800df47c9..e6437a333c124 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -139,6 +139,12 @@ New checks
Checks for presence or absence of trailing commas in enum definitions and
initializer lists.
+- New :doc:`readability-use-span-first-last
+ <clang-tidy/checks/readability/use-span-first-last>` check.
+
+ Suggests using ``std::span::first()`` and ``std::span::last()`` member
+ functions instead of equivalent ``subspan()`` calls.
+
New check aliases
^^^^^^^^^^^^^^^^^
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index 0eabd9929dc39..dcef5fbfd775c 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -430,6 +430,7 @@ Clang-Tidy Checks
:doc:`readability-uppercase-literal-suffix <readability/uppercase-literal-suffix>`, "Yes"
:doc:`readability-use-anyofallof <readability/use-anyofallof>`,
:doc:`readability-use-concise-preprocessor-directives <readability/use-concise-preprocessor-directives>`, "Yes"
+ :doc:`readability-use-span-first-last <readability/use-span-first-last>`, "Yes"
:doc:`readability-use-std-min-max <readability/use-std-min-max>`, "Yes"
:doc:`zircon-temporary-objects <zircon/temporary-objects>`,
diff --git a/clang-tools-extra/docs/clang-tidy/checks/readability/use-span-first-last.rst b/clang-tools-extra/docs/clang-tidy/checks/readability/use-span-first-last.rst
new file mode 100644
index 0000000000000..07b0c61765980
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/readability/use-span-first-last.rst
@@ -0,0 +1,24 @@
+.. title:: clang-tidy - readability-use-span-first-last
+
+readability-use-span-first-last
+===============================
+
+Suggests using ``std::span::first()`` and ``std::span::last()`` member
+functions instead of equivalent ``subspan()`` calls. These dedicated methods
+were added to C++20 to provide more expressive alternatives to common subspan
+operations.
+
+Covered scenarios:
+
+=============================== ================
+Expression Replacement
+------------------------------- ----------------
+``s.subspan(0, n)`` ``s.first(n)``
+``s.subspan(s.size() - n)`` ``s.last(n)``
+=============================== ================
+
+Non-zero offset with count (like ``subspan(1, n)``) or offset-only calls
+(like ``subspan(n)``) have no clearer equivalent using ``first()`` or
+``last()``, so these cases are not transformed.
+
+This check is only active when C++20 or later is used.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/readability/use-span-first-last.cpp b/clang-tools-extra/test/clang-tidy/checkers/readability/use-span-first-last.cpp
new file mode 100644
index 0000000000000..969a9e324940d
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/readability/use-span-first-last.cpp
@@ -0,0 +1,186 @@
+// RUN: %check_clang_tidy -std=c++20 %s readability-use-span-first-last %t
+
+namespace std {
+
+enum class byte : unsigned char {};
+
+template <typename T>
+class span {
+ T* ptr;
+ __SIZE_TYPE__ len;
+
+public:
+ span(T* p, __SIZE_TYPE__ l) : ptr(p), len(l) {}
+
+ span<T> subspan(__SIZE_TYPE__ offset) const {
+ return span(ptr + offset, len - offset);
+ }
+
+ span<T> subspan(__SIZE_TYPE__ offset, __SIZE_TYPE__ count) const {
+ return span(ptr + offset, count);
+ }
+
+ span<T> first(__SIZE_TYPE__ count) const {
+ return span(ptr, count);
+ }
+
+ span<T> last(__SIZE_TYPE__ count) const {
+ return span(ptr + (len - count), count);
+ }
+
+ __SIZE_TYPE__ size() const { return len; }
+ __SIZE_TYPE__ size_bytes() const { return len * sizeof(T); }
+};
+} // namespace std
+
+// Add here, right after the std namespace closes:
+namespace std::ranges {
+ template<typename T>
+ __SIZE_TYPE__ size(const span<T>& s) { return s.size(); }
+}
+
+void test() {
+ int arr[] = {1, 2, 3, 4, 5};
+ std::span<int> s(arr, 5);
+
+ auto sub1 = s.subspan(0, 3);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::first()' over 'subspan()'
+ // CHECK-FIXES: auto sub1 = s.first(3);
+
+ auto sub2 = s.subspan(s.size() - 2);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto sub2 = s.last(2);
+
+ __SIZE_TYPE__ n = 2;
+ auto sub3 = s.subspan(0, n);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::first()' over 'subspan()'
+ // CHECK-FIXES: auto sub3 = s.first(n);
+
+ auto sub4 = s.subspan(1, 2); // No warning
+ auto sub5 = s.subspan(2); // No warning
+
+
+#define ZERO 0
+#define TWO 2
+#define SIZE_MINUS(s, n) s.size() - n
+#define MAKE_SUBSPAN(obj, n) obj.subspan(0, n)
+#define MAKE_LAST_N(obj, n) obj.subspan(obj.size() - n)
+
+ auto sub6 = s.subspan(SIZE_MINUS(s, 2));
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto sub6 = s.last(2);
+
+ auto sub7 = MAKE_SUBSPAN(s, 3);
+ // CHECK-MESSAGES: :[[@LINE-1]]:28: warning: prefer 'span::first()' over 'subspan()'
+ // CHECK-FIXES: auto sub7 = s.first(3);
+
+ auto sub8 = MAKE_LAST_N(s, 2);
+ // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto sub8 = s.last(2);
+
+}
+
+template <typename T>
+void testTemplate() {
+ T arr[] = {1, 2, 3, 4, 5};
+ std::span<T> s(arr, 5);
+
+ auto sub1 = s.subspan(0, 3);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::first()' over 'subspan()'
+ // CHECK-FIXES: auto sub1 = s.first(3);
+
+ auto sub2 = s.subspan(s.size() - 2);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto sub2 = s.last(2);
+
+ __SIZE_TYPE__ n = 2;
+ auto sub3 = s.subspan(0, n);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::first()' over 'subspan()'
+ // CHECK-FIXES: auto sub3 = s.first(n);
+
+ auto sub4 = s.subspan(1, 2); // No warning
+ auto sub5 = s.subspan(2); // No warning
+
+ auto complex = s.subspan(0 + (s.size() - 2), 3); // No warning
+
+ auto complex2 = s.subspan(100 + (s.size() - 2)); // No warning
+}
+
+// Test instantiation
+void testInt() {
+ testTemplate<int>();
+}
+
+void test_ranges() {
+ int arr[] = {1, 2, 3, 4, 5};
+ std::span<int> s(arr, 5);
+
+ auto sub1 = s.subspan(std::ranges::size(s) - 2);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto sub1 = s.last(2);
+
+ __SIZE_TYPE__ n = 2;
+ auto sub2 = s.subspan(std::ranges::size(s) - n);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto sub2 = s.last(n);
+}
+
+void test_different_spans() {
+ int arr1[] = {1, 2, 3, 4, 5};
+ int arr2[] = {6, 7, 8, 9, 10};
+ std::span<int> s1(arr1, 5);
+ std::span<int> s2(arr2, 5);
+
+ // These should NOT trigger warnings as they use size() from a different span
+ auto sub1 = s1.subspan(s2.size() - 2); // No warning
+ auto sub2 = s2.subspan(s1.size() - 3); // No warning
+
+ // Also check with std::ranges::size
+ auto sub3 = s1.subspan(std::ranges::size(s2) - 2); // No warning
+ auto sub4 = s2.subspan(std::ranges::size(s1) - 3); // No warning
+
+ // Mixed usage should also not trigger
+ auto sub5 = s1.subspan(s2.size() - s1.size()); // No warning
+
+ // Verify that correct usage still triggers warnings
+ auto good1 = s1.subspan(s1.size() - 2);
+ // CHECK-MESSAGES: :[[@LINE-1]]:16: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto good1 = s1.last(2);
+
+ auto good2 = s2.subspan(std::ranges::size(s2) - 3);
+ // CHECK-MESSAGES: :[[@LINE-1]]:16: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto good2 = s2.last(3);
+}
+
+void test_span_of_bytes() {
+ std::byte arr[] = {std::byte{0x1}, std::byte{0x2}, std::byte{0x3},
+ std::byte{0x4}, std::byte{0x5}};
+ std::span<std::byte> s(arr, 5);
+
+ auto sub1 = s.subspan(0, 3);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::first()' over 'subspan()'
+ // CHECK-FIXES: auto sub1 = s.first(3);
+
+ auto sub2 = s.subspan(s.size() - 2);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto sub2 = s.last(2);
+
+ // size_bytes() is not the same as size() in general, so should not trigger
+ auto sub3 = s.subspan(s.size_bytes() - 2); // No warning
+}
+
+// Test uninstantiated template -- should still warn on dependent code
+template <typename T>
+void uninstantiated_template(std::span<T> s) {
+ auto sub1 = s.subspan(0, 3);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::first()' over 'subspan()'
+ // CHECK-FIXES: auto sub1 = s.first(3);
+
+ auto sub2 = s.subspan(s.size() - 2);
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: prefer 'span::last()' over 'subspan()'
+ // CHECK-FIXES: auto sub2 = s.last(2);
+
+ auto sub3 = s.subspan(1, 2); // No warning
+ auto sub4 = s.subspan(2); // No warning
+}
+
More information about the cfe-commits
mailing list