[clang] [clang][ssaf] Add cpp-bounded-buffers source transformation (PR #210457)
Ziqing Luo via cfe-commits
cfe-commits at lists.llvm.org
Sat Aug 15 16:58:06 PDT 2026
https://github.com/ziqingluo-90 updated https://github.com/llvm/llvm-project/pull/210457
>From 939d94d957220cb029d19b310110feb182c30a46 Mon Sep 17 00:00:00 2001
From: Jan Korous <jkorous at apple.com>
Date: Fri, 17 Jul 2026 13:50:27 -0700
Subject: [PATCH 1/3] [clang][ssaf] Add cpp-bounded-buffers source
transformation
Adds the first built-in `Transformation`, `cpp-bounded-buffers`, which
rewrites buffers -- raw pointers and arrays -- into bounded types
(`bounded_ptr<T>`, `bounded_array<T, N>`) using the reachability computed
by `UnsafeBufferReachableAnalysis`.
The transformation collects every reachable pointer/array declarator and
function return declared in the translation unit, then either rewrites it
or records a SARIF note explaining why it did not. Shapes that are not yet
handled -- multi-level pointers, pointer to array, references to pointers,
multi-dimensional and unbounded arrays, multi-declarator groups,
macro-spelled declarators, and trailing return types -- are reported
rather than rewritten, and a final pass reports any reachable entity that
was neither rewritten nor otherwise accounted for. Edits are validated and
committed atomically, so a declarator whose edit cannot be formed (such as
a raw array of function pointers) is reported instead of mangled.
Anchored via `CppBoundedBuffersAnchorSource` so static builds keep the
registration.
---
.../BuiltinAnchorSources.def | 1 +
.../Transformations/CppBoundedBuffers.h | 77 +++
.../SourceTransformation/CMakeLists.txt | 2 +
.../Transformations/CppBoundedBuffers.cpp | 482 ++++++++++++++++++
.../ScalableStaticAnalysis/CMakeLists.txt | 1 +
.../CppBoundedBuffersTest.cpp | 397 +++++++++++++++
6 files changed, 960 insertions(+)
create mode 100644 clang/include/clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h
create mode 100644 clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
create mode 100644 clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
diff --git a/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def b/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def
index a128ced676ed3..4af2184b9d8a6 100644
--- a/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def
+++ b/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def
@@ -19,6 +19,7 @@
ANCHOR(AnalysisRegistryAnchorSource)
ANCHOR(CallGraphExtractorAnchorSource)
ANCHOR(CallGraphJSONFormatAnchorSource)
+ANCHOR(CppBoundedBuffersAnchorSource)
ANCHOR(EntitySourceLocationExtractorAnchorSource)
ANCHOR(JSONFormatAnchorSource)
ANCHOR(TypeConstrainedPointersAnchorSource)
diff --git a/clang/include/clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h b/clang/include/clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h
new file mode 100644
index 0000000000000..5b37282b313cf
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h
@@ -0,0 +1,77 @@
+//===- CppBoundedBuffers.h --------------------------------------*- 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
+//
+//===----------------------------------------------------------------------===//
+//
+// The cpp-bounded-buffers transformation rewrites buffers -- raw pointers and
+// arrays -- reachable from unsafe buffer usage into bounded types
+// (bounded_ptr<T>, bounded_array<T, N>). Reachable declarators that are not
+// rewritten are reported instead, so no reachable buffer is silently left raw.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSIS_SOURCETRANSFORMATION_TRANSFORMATIONS_CPPBOUNDEDBUFFERS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSIS_SOURCETRANSFORMATION_TRANSFORMATIONS_CPPBOUNDEDBUFFERS_H
+
+#include "clang/AST/Type.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/Transformation.h"
+#include "llvm/ADT/SmallSet.h"
+#include "llvm/ADT/StringRef.h"
+#include <optional>
+#include <string>
+
+namespace clang {
+class ASTContext;
+} // namespace clang
+
+namespace clang::ssaf {
+
+/// The bounded type a raw declarator is rewritten to.
+enum class BoundedType { Ptr, Array };
+
+/// Why a reachable declarator was reported instead of rewritten.
+enum class ReportReason {
+ MultiLevelPointer,
+ PointerToArray,
+ ReferenceToPointer,
+ MultiDimensionalArray,
+ IncompleteArray,
+ UnreproducibleType,
+ DeclarationGroup,
+ MacroExpansion,
+ TrailingReturnType,
+ EmissionFailed,
+ NotTransformed,
+};
+
+/// Returns the report message for \p Reason.
+llvm::StringRef messageFor(ReportReason Reason);
+
+/// The outcome of classifying a declared type against the reachable pointer
+/// levels of its entity: a bounded-type rewrite, a report reason, or neither.
+struct ClassifyResult {
+ std::optional<BoundedType> NewType;
+ // Pointee/element spelling; meaningful only when NewType is set.
+ std::string InnerSpelling;
+ std::optional<ReportReason> Skip;
+};
+
+/// Classifies the declared type \p T of a reachable entity. \p ReachableLevels
+/// holds the entity's reachable pointer levels (1-based, outermost is level 1).
+ClassifyResult
+classifyDeclType(QualType T, const llvm::SmallSet<unsigned, 4> &ReachableLevels,
+ const ASTContext &Ctx);
+
+class CppBoundedBuffers : public Transformation {
+public:
+ using Transformation::Transformation;
+
+ void HandleTranslationUnit(clang::ASTContext &Ctx) override;
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSIS_SOURCETRANSFORMATION_TRANSFORMATIONS_CPPBOUNDEDBUFFERS_H
diff --git a/clang/lib/ScalableStaticAnalysis/SourceTransformation/CMakeLists.txt b/clang/lib/ScalableStaticAnalysis/SourceTransformation/CMakeLists.txt
index 25db2fb7eca7d..96e85ec3a50a8 100644
--- a/clang/lib/ScalableStaticAnalysis/SourceTransformation/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysis/SourceTransformation/CMakeLists.txt
@@ -5,11 +5,13 @@ set(LLVM_LINK_COMPONENTS
add_clang_library(clangScalableStaticAnalysisSourceTransformation
SARIFTransformationReportFormat.cpp
TransformationRegistry.cpp
+ Transformations/CppBoundedBuffers.cpp
YAMLSourceEditFormat.cpp
LINK_LIBS
clangAST
clangBasic
+ clangLex
clangScalableStaticAnalysisCore
clangToolingCore
)
diff --git a/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp b/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
new file mode 100644
index 0000000000000..25816420e504a
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
@@ -0,0 +1,482 @@
+//===- CppBoundedBuffers.cpp ----------------------------------------------===//
+//
+// 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 "clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclBase.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/DynamicRecursiveASTVisitor.h"
+#include "clang/AST/Type.h"
+#include "clang/AST/TypeLoc.h"
+#include "clang/Basic/SourceLocation.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Lex/Lexer.h"
+#include "clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h"
+#include "clang/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.h"
+#include "clang/ScalableStaticAnalysis/Core/ASTEntityMapping.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityIdTable.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityName.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/TransformationRegistry.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
+#include <cassert>
+#include <map>
+#include <string>
+
+using namespace clang;
+using namespace clang::ssaf;
+
+static constexpr llvm::StringLiteral SkippedRuleId =
+ "cpp-bounded-buffers-skipped";
+
+namespace {
+
+/// A declarator whose type can carry pointer levels.
+bool isCandidateType(QualType T) {
+ QualType U = T.getNonReferenceType();
+ return U->isPointerType() || U->isArrayType();
+}
+
+std::string spell(QualType T, const ASTContext &Ctx) {
+ return T.getAsString(Ctx.getPrintingPolicy());
+}
+
+/// Whether \p T can be re-emitted as written. Anonymous records and lambdas
+/// have no usable spelling.
+bool isReproducible(QualType T) {
+ const auto *RT = T->getAs<RecordType>();
+ if (!RT)
+ return true;
+ const RecordDecl *RD = RT->getDecl();
+ if (const auto *CXXRD = dyn_cast<CXXRecordDecl>(RD))
+ if (CXXRD->isLambda())
+ return false;
+ return RD->getIdentifier() || RD->getTypedefNameForAnonDecl();
+}
+
+std::string cvPrefix(QualType T) {
+ std::string Prefix;
+ if (T.isLocalConstQualified())
+ Prefix += "const ";
+ if (T.isLocalVolatileQualified())
+ Prefix += "volatile ";
+ return Prefix;
+}
+
+std::string renderNewType(const ClassifyResult &R, QualType T,
+ const ASTContext &Ctx) {
+ if (*R.NewType == BoundedType::Ptr)
+ return cvPrefix(T) + "bounded_ptr<" + R.InnerSpelling + "> ";
+ const auto *CAT = Ctx.getAsConstantArrayType(T);
+ std::string N = std::to_string(CAT->getSize().getZExtValue());
+ return "bounded_array<" + R.InnerSpelling + ", " + N + "> ";
+}
+
+/// Whether another declarator in \p D's lexical context shares its type
+/// specifier, i.e. \p D is one declarator of a multi-declarator group.
+bool sharesTypeSpecifier(const DeclaratorDecl *D) {
+ const TypeSourceInfo *TSI = D->getTypeSourceInfo();
+ const DeclContext *DC = D->getLexicalDeclContext();
+ if (!TSI || !DC)
+ return false;
+ SourceLocation Begin = TSI->getTypeLoc().getBeginLoc();
+ for (const Decl *Sibling : DC->decls()) {
+ if (Sibling == D)
+ continue;
+ const auto *Other = dyn_cast<DeclaratorDecl>(Sibling);
+ if (Other && Other->getTypeSourceInfo() &&
+ Other->getTypeSourceInfo()->getTypeLoc().getBeginLoc() == Begin)
+ return true;
+ }
+ return false;
+}
+
+bool hasTrailingReturnType(const FunctionDecl *FD) {
+ const auto *FPT = FD->getType()->getAs<FunctionProtoType>();
+ return FPT && FPT->hasTrailingReturn();
+}
+
+CharSourceRange declTypeRange(const DeclaratorDecl *D) {
+ if (const TypeSourceInfo *TSI = D->getTypeSourceInfo())
+ return CharSourceRange::getTokenRange(TSI->getTypeLoc().getSourceRange());
+ return CharSourceRange::getTokenRange(D->getSourceRange());
+}
+
+/// A leading cv-qualifier keyword (e.g. the `const` in `const char *`) is not
+/// covered by the type-loc's begin location; extend \p TypeBegin left over it.
+SourceLocation extendOverLeadingQualifiers(SourceLocation TypeBegin,
+ const ASTContext &Ctx) {
+ const SourceManager &SM = Ctx.getSourceManager();
+ const LangOptions &LangOpts = Ctx.getLangOpts();
+ while (std::optional<Token> Prev = Lexer::findPreviousToken(
+ TypeBegin, SM, LangOpts, /*IncludeComments=*/false)) {
+ // findPreviousToken lexes raw tokens, so keywords arrive as identifiers.
+ if (!Prev->is(tok::raw_identifier))
+ break;
+ StringRef Text = Prev->getRawIdentifier();
+ if (Text != "const" && Text != "volatile")
+ break;
+ TypeBegin = Prev->getLocation();
+ }
+ return TypeBegin;
+}
+
+/// Reverse index from the whole-program reachability result onto entity names,
+/// so a declaration in this TU can look up its reachable pointer levels.
+class ReachabilityMap {
+ const std::map<EntityId, EntityPointerLevelSet> &Reachables;
+ std::map<EntityName, EntityId> NameToId;
+
+public:
+ ReachabilityMap(const WPASuite &Suite,
+ const std::map<EntityId, EntityPointerLevelSet> &Reachables)
+ : Reachables(Reachables) {
+ Suite.getIdTable().forEach([this](const EntityName &Name, EntityId Id) {
+ NameToId.emplace(Name, Id);
+ });
+ }
+
+ llvm::SmallSet<unsigned, 4> levelsFor(std::optional<EntityName> Name) const {
+ llvm::SmallSet<unsigned, 4> Levels;
+ if (!Name)
+ return Levels;
+ auto NameIt = NameToId.find(*Name);
+ if (NameIt == NameToId.end())
+ return Levels;
+ auto ReachIt = Reachables.find(NameIt->second);
+ if (ReachIt == Reachables.end())
+ return Levels;
+ for (const EntityPointerLevel &EPL : ReachIt->second)
+ Levels.insert(EPL.getPointerLevel());
+ return Levels;
+ }
+};
+
+struct Candidate {
+ llvm::SmallSet<unsigned, 4> Levels;
+ bool AccountedFor = false;
+};
+
+using DeclLevels = std::map<const Decl *, Candidate>;
+using ReturnLevels = std::map<const FunctionDecl *, Candidate>;
+
+/// Collects the reachable pointer/array declarators and function returns
+/// declared in this TU.
+class CollectVisitor : public DynamicRecursiveASTVisitor {
+public:
+ CollectVisitor(const ReachabilityMap &Reach, DeclLevels &Decls,
+ ReturnLevels &Returns)
+ : Reach(Reach), Decls(Decls), Returns(Returns) {}
+
+ bool VisitVarDecl(VarDecl *D) override {
+ collect(D, D->getType(), getEntityName(D));
+ return true;
+ }
+
+ bool VisitFieldDecl(FieldDecl *D) override {
+ collect(D, D->getType(), getEntityName(D));
+ return true;
+ }
+
+ bool VisitFunctionDecl(FunctionDecl *FD) override {
+ if (!FD->isTemplated() && isCandidateType(FD->getReturnType())) {
+ llvm::SmallSet<unsigned, 4> Levels =
+ Reach.levelsFor(getEntityNameForReturn(FD));
+ if (!Levels.empty())
+ Returns[FD].Levels = std::move(Levels);
+ }
+ return true;
+ }
+
+private:
+ void collect(const Decl *D, QualType T, std::optional<EntityName> Name) {
+ if (D->isTemplated() || !isCandidateType(T))
+ return;
+ llvm::SmallSet<unsigned, 4> Levels = Reach.levelsFor(Name);
+ if (!Levels.empty())
+ Decls[D].Levels = std::move(Levels);
+ }
+
+ const ReachabilityMap &Reach;
+ DeclLevels &Decls;
+ ReturnLevels &Returns;
+};
+
+/// Rewrites or reports every collected declarator and function return.
+class RewriteVisitor : public DynamicRecursiveASTVisitor {
+public:
+ RewriteVisitor(ASTContext &Ctx, DeclLevels &Decls, ReturnLevels &Returns,
+ SourceEditEmitter &Edits, TransformationReportEmitter &Report)
+ : Ctx(Ctx), Decls(Decls), Returns(Returns), Edits(Edits), Report(Report) {
+ }
+
+ bool VisitVarDecl(VarDecl *D) override {
+ processDecl(D, D->getType());
+ return true;
+ }
+
+ bool VisitFieldDecl(FieldDecl *D) override {
+ processDecl(D, D->getType());
+ return true;
+ }
+
+ bool VisitFunctionDecl(FunctionDecl *FD) override {
+ auto It = Returns.find(FD);
+ if (It == Returns.end())
+ return true;
+ Candidate &Cand = It->second;
+ if (hasTrailingReturnType(FD))
+ return account(Cand, FD, ReportReason::TrailingReturnType);
+
+ SourceLocation TypeBegin = FD->getReturnTypeSourceRange().getBegin();
+ SourceLocation NameLoc = FD->getLocation();
+ if (TypeBegin.isMacroID() || NameLoc.isMacroID())
+ return account(Cand, FD, ReportReason::MacroExpansion);
+
+ ClassifyResult R = classifyDeclType(FD->getReturnType(), Cand.Levels, Ctx);
+ if (R.Skip)
+ return account(Cand, FD, *R.Skip);
+ if (R.NewType) {
+ bool Ok = emit(TypeBegin, NameLoc, FD->getReturnType(), R,
+ /*ArrayTypeLoc=*/std::nullopt);
+ return account(Cand, FD,
+ Ok ? std::nullopt
+ : std::optional(ReportReason::EmissionFailed));
+ }
+ return true;
+ }
+
+private:
+ void processDecl(DeclaratorDecl *D, QualType T) {
+ auto It = Decls.find(D);
+ if (It == Decls.end())
+ return;
+ Candidate &Cand = It->second;
+ if (sharesTypeSpecifier(D))
+ return (void)account(Cand, D, ReportReason::DeclarationGroup);
+
+ const TypeSourceInfo *TSI = D->getTypeSourceInfo();
+ SourceLocation TypeBegin =
+ TSI ? TSI->getTypeLoc().getBeginLoc() : SourceLocation();
+ SourceLocation NameLoc = D->getLocation();
+ if (TypeBegin.isMacroID() || NameLoc.isMacroID())
+ return (void)account(Cand, D, ReportReason::MacroExpansion);
+
+ ClassifyResult R = classifyDeclType(T, Cand.Levels, Ctx);
+ if (R.Skip)
+ return (void)account(Cand, D, *R.Skip);
+ if (R.NewType) {
+ std::optional<TypeLoc> ArrayTypeLoc;
+ if (*R.NewType == BoundedType::Array && TSI)
+ ArrayTypeLoc = TSI->getTypeLoc();
+ bool Ok = emit(TypeBegin, NameLoc, T, R, ArrayTypeLoc);
+ account(Cand, D,
+ Ok ? std::nullopt : std::optional(ReportReason::EmissionFailed));
+ }
+ }
+
+ /// Emits the type-token replacement (and, for arrays, deletes the trailing
+ /// extent). Returns false without emitting anything if a valid,
+ /// self-contained edit cannot be formed.
+ bool emit(SourceLocation TypeBegin, SourceLocation NameLoc, QualType T,
+ const ClassifyResult &R, std::optional<TypeLoc> ForArray) {
+ const SourceManager &SM = Ctx.getSourceManager();
+ if (TypeBegin.isValid() && !TypeBegin.isMacroID())
+ TypeBegin = extendOverLeadingQualifiers(TypeBegin, Ctx);
+ if (TypeBegin.isInvalid() || NameLoc.isInvalid() || TypeBegin.isMacroID() ||
+ NameLoc.isMacroID() ||
+ SM.getFileID(TypeBegin) != SM.getFileID(NameLoc) ||
+ SM.getFileOffset(NameLoc) <= SM.getFileOffset(TypeBegin))
+ return false;
+
+ llvm::SmallVector<tooling::Replacement, 2> Edited;
+ Edited.emplace_back(SM, CharSourceRange::getCharRange(TypeBegin, NameLoc),
+ renderNewType(R, T, Ctx), Ctx.getLangOpts());
+
+ if (ForArray) {
+ ArrayTypeLoc ATL = ForArray->getAs<ArrayTypeLoc>();
+ if (!ATL)
+ return false;
+ SourceLocation LBracket = ATL.getLBracketLoc();
+ SourceLocation RBracket = ATL.getRBracketLoc();
+ // A clean array declarator ends at its closing bracket; otherwise the
+ // element spelling wraps the name (e.g. an array of function pointers)
+ // and cannot be rewritten by stripping a trailing extent.
+ if (LBracket.isInvalid() || RBracket.isInvalid() ||
+ ForArray->getEndLoc() != RBracket)
+ return false;
+ Edited.emplace_back(SM,
+ CharSourceRange::getTokenRange(LBracket, RBracket),
+ "", Ctx.getLangOpts());
+ }
+
+ for (const tooling::Replacement &Repl : Edited)
+ if (!Repl.isApplicable())
+ return false;
+ for (tooling::Replacement &Repl : Edited)
+ Edits.addReplacement(std::move(Repl));
+ return true;
+ }
+
+ /// Marks \p Cand accounted for, reporting \p Reason if one is given.
+ bool account(Candidate &Cand, const DeclaratorDecl *D,
+ std::optional<ReportReason> Reason) {
+ Cand.AccountedFor = true;
+ if (Reason)
+ Report.addResult(SkippedRuleId, SarifResultLevel::Note, declTypeRange(D),
+ messageFor(*Reason));
+ return true;
+ }
+
+ ASTContext &Ctx;
+ DeclLevels &Decls;
+ ReturnLevels &Returns;
+ SourceEditEmitter &Edits;
+ TransformationReportEmitter &Report;
+};
+
+} // namespace
+
+namespace clang::ssaf {
+
+llvm::StringRef messageFor(ReportReason Reason) {
+ switch (Reason) {
+ case ReportReason::MultiLevelPointer:
+ return "multi-level pointer indirection is not yet rewritten";
+ case ReportReason::PointerToArray:
+ return "pointer to array is not yet rewritten";
+ case ReportReason::ReferenceToPointer:
+ return "reference to pointer is not yet rewritten";
+ case ReportReason::MultiDimensionalArray:
+ return "multi-dimensional array is not yet rewritten";
+ case ReportReason::IncompleteArray:
+ return "array of unknown bound is not yet rewritten";
+ case ReportReason::UnreproducibleType:
+ return "type spelling cannot be reproduced";
+ case ReportReason::DeclarationGroup:
+ return "declarator of a multi-declarator group is not yet rewritten";
+ case ReportReason::MacroExpansion:
+ return "declarator spelled through a macro is not yet rewritten";
+ case ReportReason::TrailingReturnType:
+ return "trailing return type is not yet rewritten";
+ case ReportReason::EmissionFailed:
+ return "no source edit could be formed for this declarator";
+ case ReportReason::NotTransformed:
+ return "reachable buffer was not transformed";
+ }
+ llvm_unreachable("unhandled ReportReason");
+}
+
+ClassifyResult
+classifyDeclType(QualType T, const llvm::SmallSet<unsigned, 4> &ReachableLevels,
+ const ASTContext &Ctx) {
+ ClassifyResult R;
+ if (!ReachableLevels.count(1))
+ return R;
+
+ // A deeper indirection level is reachable too; that is a multi-level rewrite,
+ // which is not yet supported.
+ if (llvm::any_of(ReachableLevels, [](unsigned L) { return L > 1; })) {
+ R.Skip = ReportReason::MultiLevelPointer;
+ return R;
+ }
+
+ if (T->isReferenceType()) {
+ QualType Pointee = T.getNonReferenceType();
+ if (Pointee->isPointerType() || Pointee->isArrayType())
+ R.Skip = ReportReason::ReferenceToPointer;
+ return R;
+ }
+
+ if (const auto *PT = T->getAs<PointerType>()) {
+ QualType Pointee = PT->getPointeeType();
+ if (Pointee->isFunctionType()) {
+ assert(false &&
+ "function pointer entities are not expected to be reachable");
+ return R;
+ }
+ if (Pointee->isPointerType()) {
+ R.Skip = ReportReason::MultiLevelPointer;
+ return R;
+ }
+ if (Pointee->isArrayType()) {
+ R.Skip = ReportReason::PointerToArray;
+ return R;
+ }
+ if (!isReproducible(Pointee)) {
+ R.Skip = ReportReason::UnreproducibleType;
+ return R;
+ }
+ R.NewType = BoundedType::Ptr;
+ R.InnerSpelling = Pointee->isVoidType() ? "char" : spell(Pointee, Ctx);
+ return R;
+ }
+
+ if (const auto *CAT = Ctx.getAsConstantArrayType(T)) {
+ QualType Element = CAT->getElementType();
+ if (Element->isArrayType()) {
+ R.Skip = ReportReason::MultiDimensionalArray;
+ return R;
+ }
+ if (!isReproducible(Element)) {
+ R.Skip = ReportReason::UnreproducibleType;
+ return R;
+ }
+ R.NewType = BoundedType::Array;
+ R.InnerSpelling = spell(Element, Ctx);
+ return R;
+ }
+
+ if (T->isArrayType())
+ R.Skip = ReportReason::IncompleteArray;
+ return R;
+}
+
+void CppBoundedBuffers::HandleTranslationUnit(ASTContext &Ctx) {
+ auto Reachable = Suite.get<UnsafeBufferReachableAnalysisResult>();
+ if (!Reachable) {
+ llvm::consumeError(Reachable.takeError());
+ return;
+ }
+
+ ReachabilityMap Reach(Suite, Reachable->Reachables);
+ DeclLevels Decls;
+ ReturnLevels Returns;
+
+ Decl *TU = Ctx.getTranslationUnitDecl();
+ CollectVisitor(Reach, Decls, Returns).TraverseDecl(TU);
+ RewriteVisitor(Ctx, Decls, Returns, Edits, Report).TraverseDecl(TU);
+
+ // Every reachable buffer in this TU is either rewritten or reported; a
+ // leftover means it was neither, which must still be surfaced.
+ for (const auto &[D, Cand] : Decls)
+ if (!Cand.AccountedFor)
+ Report.addResult(SkippedRuleId, SarifResultLevel::Note,
+ declTypeRange(cast<DeclaratorDecl>(D)),
+ messageFor(ReportReason::NotTransformed));
+ for (const auto &[FD, Cand] : Returns)
+ if (!Cand.AccountedFor)
+ Report.addResult(
+ SkippedRuleId, SarifResultLevel::Note,
+ CharSourceRange::getTokenRange(FD->getReturnTypeSourceRange()),
+ messageFor(ReportReason::NotTransformed));
+}
+
+} // namespace clang::ssaf
+
+namespace clang::ssaf {
+// NOLINTNEXTLINE(misc-use-internal-linkage)
+volatile int CppBoundedBuffersAnchorSource = 0;
+} // namespace clang::ssaf
+
+static clang::ssaf::TransformationRegistry::Add<CppBoundedBuffers>
+ RegisterCppBoundedBuffers("cpp-bounded-buffers",
+ "Rewrites buffers into bounded types");
diff --git a/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
index ed3b57168b069..0f3bf9ad4512b 100644
--- a/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
@@ -27,6 +27,7 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
Serialization/JSONFormatTest/LUSummaryTest.cpp
Serialization/JSONFormatTest/SharedLexicalRepresentationFormatTest.cpp
Serialization/JSONFormatTest/TUSummaryTest.cpp
+ SourceTransformation/CppBoundedBuffersTest.cpp
SourceTransformation/EmitterTest.cpp
SourceTransformation/RegistryTest.cpp
SourceTransformation/SARIFFormatTest.cpp
diff --git a/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp b/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
new file mode 100644
index 0000000000000..ca6e5e3894e70
--- /dev/null
+++ b/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
@@ -0,0 +1,397 @@
+//===- CppBoundedBuffersTest.cpp ------------------------------------------===//
+//
+// 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 "clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h"
+#include "FindDecl.h"
+#include "TestFixture.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/Basic/Sarif.h"
+#include "clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h"
+#include "clang/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.h"
+#include "clang/ScalableStaticAnalysis/Core/ASTEntityMapping.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityIdTable.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityName.h"
+#include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/WPASuite.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/SourceEditEmitter.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/TransformationReportEmitter.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/SmallSet.h"
+#include "llvm/Support/Error.h"
+#include "gtest/gtest.h"
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+using namespace clang;
+using namespace clang::ssaf;
+
+namespace {
+
+class RecordingEditEmitter : public SourceEditEmitter {
+public:
+ std::vector<tooling::Replacement> Replacements;
+
+ void addReplacement(tooling::Replacement R) override {
+ Replacements.push_back(std::move(R));
+ }
+};
+
+class RecordingReportEmitter : public TransformationReportEmitter {
+public:
+ struct Entry {
+ std::string RuleId;
+ SarifResultLevel Level;
+ std::string Message;
+ };
+ std::vector<Entry> Results;
+
+ void addResult(StringRef RuleId, SarifResultLevel Level, CharSourceRange,
+ StringRef Message) override {
+ Results.push_back({RuleId.str(), Level, Message.str()});
+ }
+};
+
+std::optional<EntityName> varEntity(StringRef Name, ASTContext &Ctx) {
+ return getEntityName(findDeclByName<VarDecl>(Name, Ctx));
+}
+
+std::optional<EntityName> fieldEntity(StringRef Name, ASTContext &Ctx) {
+ return getEntityName(findDeclByName<FieldDecl>(Name, Ctx));
+}
+
+std::optional<EntityName> paramEntity(StringRef Fn, unsigned Idx,
+ ASTContext &Ctx) {
+ const FunctionDecl *FD = findFnByName(Fn, Ctx);
+ return FD ? getEntityName(FD->getParamDecl(Idx)) : std::nullopt;
+}
+
+std::optional<EntityName> returnEntity(StringRef Fn, ASTContext &Ctx) {
+ return getEntityNameForReturn(findFnByName(Fn, Ctx));
+}
+
+struct Captured {
+ std::string Rewritten;
+ std::vector<RecordingReportEmitter::Entry> Reports;
+};
+
+class CppBoundedBuffersTest : public TestFixture {
+protected:
+ using EntityFn = llvm::function_ref<std::optional<EntityName>(ASTContext &)>;
+ using MarkFn = llvm::function_ref<void(
+ ASTContext &, WPASuite &, UnsafeBufferReachableAnalysisResult &)>;
+
+ // Marks the entity \p Name reachable at \p Levels in \p Result.
+ static void markReachable(WPASuite &Suite,
+ UnsafeBufferReachableAnalysisResult &Result,
+ std::optional<EntityName> Name,
+ ArrayRef<unsigned> Levels) {
+ if (!Name || Levels.empty())
+ return;
+ EntityId Id = getIdTable(Suite).getId(*Name);
+ EntityPointerLevelSet Set;
+ for (unsigned Level : Levels)
+ Set.insert(buildEntityPointerLevel(Id, Level));
+ Result.Reachables[Id] = std::move(Set);
+ }
+
+ // Parses \p Code, lets \p Mark populate the reachable result, runs the
+ // transformation, and returns the rewritten source and report entries.
+ Captured runMarked(StringRef Code, MarkFn Mark) {
+ std::unique_ptr<ASTUnit> AST = tooling::buildASTFromCode(Code);
+ ASTContext &Ctx = AST->getASTContext();
+
+ WPASuite Suite = makeWPASuite();
+ auto Result = std::make_unique<UnsafeBufferReachableAnalysisResult>();
+ Mark(Ctx, Suite, *Result);
+ getData(Suite)[UnsafeBufferReachableAnalysisResult::analysisName()] =
+ std::move(Result);
+
+ RecordingEditEmitter Edits;
+ RecordingReportEmitter Report;
+ CppBoundedBuffers(Suite, Edits, Report).HandleTranslationUnit(Ctx);
+
+ tooling::Replacements Replacements;
+ for (const tooling::Replacement &R : Edits.Replacements)
+ cantFail(Replacements.add(R));
+ return {cantFail(tooling::applyAllReplacements(Code, Replacements)),
+ std::move(Report.Results)};
+ }
+
+ Captured run(StringRef Code, EntityFn EntityOf, ArrayRef<unsigned> Levels) {
+ return runMarked(Code, [&](ASTContext &Ctx, WPASuite &Suite,
+ UnsafeBufferReachableAnalysisResult &Result) {
+ markReachable(Suite, Result, EntityOf(Ctx), Levels);
+ });
+ }
+};
+
+//===----------------------------------------------------------------------===//
+// Rewrites: assert the rewritten source and that nothing is reported.
+//===----------------------------------------------------------------------===//
+
+TEST_F(CppBoundedBuffersTest, PointerLocal) {
+ Captured C = run("void f() { int *p; }",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "void f() { bounded_ptr<int> p; }");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, PointerParameter) {
+ Captured C =
+ run("void f(int *p);",
+ [](ASTContext &Ctx) { return paramEntity("f", 0, Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "void f(bounded_ptr<int> p);");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ConstQualifiedPointee) {
+ Captured C = run("const char *s;",
+ [](ASTContext &Ctx) { return varEntity("s", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "bounded_ptr<const char> s;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, VoidPointer) {
+ Captured C =
+ run("void *p;", [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "bounded_ptr<char> p;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ArrayField) {
+ Captured C = run("struct S { int a[10]; };",
+ [](ASTContext &Ctx) { return fieldEntity("a", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "struct S { bounded_array<int, 10> a; };");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, FunctionReturn) {
+ Captured C =
+ run("int *foo();",
+ [](ASTContext &Ctx) { return returnEntity("foo", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "bounded_ptr<int> foo();");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, GlobalPointer) {
+ Captured C =
+ run("int *g;", [](ASTContext &Ctx) { return varEntity("g", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "bounded_ptr<int> g;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, PointerField) {
+ Captured C = run("struct S { int *p; };",
+ [](ASTContext &Ctx) { return fieldEntity("p", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "struct S { bounded_ptr<int> p; };");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ArrayOfPointers) {
+ Captured C = run("int *a[10];",
+ [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "bounded_array<int *, 10> a;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ArrayOfFunctionPointers) {
+ // A function-pointer element is accepted (not rejected like a bare function
+ // pointer); the typedef keeps the declarator a clean prefix + [N] suffix.
+ Captured C = run("typedef void (*FP)(); FP fps[4];",
+ [](ASTContext &Ctx) { return varEntity("fps", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "typedef void (*FP)(); bounded_array<FP, 4> fps;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+//===----------------------------------------------------------------------===//
+// Skips: assert no edit and a single reported reason.
+//===----------------------------------------------------------------------===//
+
+void expectSkip(const Captured &C, StringRef Original, ReportReason Reason) {
+ EXPECT_EQ(C.Rewritten, Original);
+ ASSERT_EQ(C.Reports.size(), 1u);
+ EXPECT_EQ(C.Reports[0].Level, SarifResultLevel::Note);
+ EXPECT_EQ(C.Reports[0].Message, messageFor(Reason).str());
+}
+
+TEST_F(CppBoundedBuffersTest, MultiLevelPointer) {
+ StringRef Code = "int **pp;";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("pp", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::MultiLevelPointer);
+}
+
+TEST_F(CppBoundedBuffersTest, MultiDimensionalArray) {
+ StringRef Code = "int a[2][3];";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::MultiDimensionalArray);
+}
+
+TEST_F(CppBoundedBuffersTest, IncompleteArray) {
+ StringRef Code = "extern int a[];";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::IncompleteArray);
+}
+
+TEST_F(CppBoundedBuffersTest, PointerToArray) {
+ StringRef Code = "int (*p)[10];";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::PointerToArray);
+}
+
+TEST_F(CppBoundedBuffersTest, ReferenceToPointer) {
+ StringRef Code = "void f(int *&r);";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return paramEntity("f", 0, Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::ReferenceToPointer);
+}
+
+TEST_F(CppBoundedBuffersTest, UnreproducibleType) {
+ StringRef Code = "struct { int x; } *p;";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::UnreproducibleType);
+}
+
+TEST_F(CppBoundedBuffersTest, DeclarationGroup) {
+ // Both declarators share one type specifier; comma-group splitting is not
+ // yet supported, so both are reported and neither is rewritten.
+ Captured C =
+ runMarked("int *p, *q;", [](ASTContext &Ctx, WPASuite &Suite,
+ UnsafeBufferReachableAnalysisResult &Result) {
+ markReachable(Suite, Result, varEntity("p", Ctx), {1});
+ markReachable(Suite, Result, varEntity("q", Ctx), {1});
+ });
+ EXPECT_EQ(C.Rewritten, "int *p, *q;");
+ ASSERT_EQ(C.Reports.size(), 2u);
+ for (const auto &R : C.Reports) {
+ EXPECT_EQ(R.Level, SarifResultLevel::Note);
+ EXPECT_EQ(R.Message, messageFor(ReportReason::DeclarationGroup).str());
+ }
+}
+
+TEST_F(CppBoundedBuffersTest, MacroSpelledDeclarator) {
+ StringRef Code = "#define PTR int *\nPTR p;\n";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::MacroExpansion);
+}
+
+TEST_F(CppBoundedBuffersTest, TrailingReturnType) {
+ StringRef Code = "auto f() -> int *;";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return returnEntity("f", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::TrailingReturnType);
+}
+
+TEST_F(CppBoundedBuffersTest, EmissionFailureOnRawFunctionPointerArray) {
+ // A raw array-of-function-pointers has no clean prefix + [N] suffix, so the
+ // edit cannot be formed and the entity is reported rather than mangled.
+ StringRef Code = "void (*fps[4])();";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("fps", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::EmissionFailed);
+}
+
+//===----------------------------------------------------------------------===//
+// Completeness and negative cases.
+//===----------------------------------------------------------------------===//
+
+TEST_F(CppBoundedBuffersTest, UnaccountedReachableIsReported) {
+ // A single pointer has only level 1; marking level 2 reachable leaves the
+ // entity neither rewritten nor shape-skipped, so the sweep reports it.
+ StringRef Code = "int *p;";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {2});
+ expectSkip(C, Code, ReportReason::NotTransformed);
+}
+
+TEST_F(CppBoundedBuffersTest, NotReachable) {
+ StringRef Code = "int *p;";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {});
+ EXPECT_EQ(C.Rewritten, Code);
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, RewriteAndReportCoexist) {
+ // A rewritten entity and a reported one in the same TU: the rewrite happens
+ // and only the not-rewritten entity is reported.
+ Captured C = runMarked(
+ "int *good; int **bad;", [](ASTContext &Ctx, WPASuite &Suite,
+ UnsafeBufferReachableAnalysisResult &Result) {
+ markReachable(Suite, Result, varEntity("good", Ctx), {1});
+ markReachable(Suite, Result, varEntity("bad", Ctx), {1});
+ });
+ EXPECT_EQ(C.Rewritten, "bounded_ptr<int> good; int **bad;");
+ ASSERT_EQ(C.Reports.size(), 1u);
+ EXPECT_EQ(C.Reports[0].Message,
+ messageFor(ReportReason::MultiLevelPointer).str());
+}
+
+//===----------------------------------------------------------------------===//
+// Classifier unit tests: pin the level direction and message coverage.
+//===----------------------------------------------------------------------===//
+
+QualType typeOf(StringRef Name, ASTContext &Ctx) {
+ return findDeclByName<VarDecl>(Name, Ctx)->getType();
+}
+
+TEST_F(CppBoundedBuffersTest, ClassifyRewritesOutermostReachablePointer) {
+ auto AST = tooling::buildASTFromCode("int *p;");
+ llvm::SmallSet<unsigned, 4> Levels;
+ Levels.insert(1);
+ ClassifyResult R = classifyDeclType(typeOf("p", AST->getASTContext()), Levels,
+ AST->getASTContext());
+ ASSERT_TRUE(R.NewType.has_value());
+ EXPECT_EQ(*R.NewType, BoundedType::Ptr);
+ EXPECT_EQ(R.InnerSpelling, "int");
+ EXPECT_FALSE(R.Skip.has_value());
+}
+
+TEST_F(CppBoundedBuffersTest, ClassifyIgnoresInnerOnlyReachablePointer) {
+ auto AST = tooling::buildASTFromCode("int *p;");
+ llvm::SmallSet<unsigned, 4> Levels;
+ Levels.insert(2);
+ ClassifyResult R = classifyDeclType(typeOf("p", AST->getASTContext()), Levels,
+ AST->getASTContext());
+ EXPECT_FALSE(R.NewType.has_value());
+ EXPECT_FALSE(R.Skip.has_value());
+}
+
+TEST_F(CppBoundedBuffersTest, ClassifyMultiLevelPointerIsSkipped) {
+ auto AST = tooling::buildASTFromCode("int **pp;");
+ llvm::SmallSet<unsigned, 4> Levels;
+ Levels.insert(1);
+ ClassifyResult R = classifyDeclType(typeOf("pp", AST->getASTContext()),
+ Levels, AST->getASTContext());
+ EXPECT_FALSE(R.NewType.has_value());
+ ASSERT_TRUE(R.Skip.has_value());
+ EXPECT_EQ(*R.Skip, ReportReason::MultiLevelPointer);
+}
+
+TEST_F(CppBoundedBuffersTest, MessageForIsNonEmpty) {
+ for (ReportReason Reason :
+ {ReportReason::MultiLevelPointer, ReportReason::PointerToArray,
+ ReportReason::ReferenceToPointer, ReportReason::MultiDimensionalArray,
+ ReportReason::IncompleteArray, ReportReason::UnreproducibleType,
+ ReportReason::DeclarationGroup, ReportReason::MacroExpansion,
+ ReportReason::TrailingReturnType, ReportReason::EmissionFailed,
+ ReportReason::NotTransformed})
+ EXPECT_FALSE(messageFor(Reason).empty());
+}
+
+} // namespace
>From 54e8b298437bc83771b3717fa77354cc0424bd4a Mon Sep 17 00:00:00 2001
From: Ziqing Luo <ziqing_luo at apple.com>
Date: Fri, 14 Aug 2026 22:51:24 -0700
Subject: [PATCH 2/3] Take over from Jan.
The main change is to narrow the rewrite range:
For pointers, let T mark the spelling of the pointee type according to
its TypeLoc:
```
other-specifiers cv-qualifiers T * other-specifiers pointer-name
|--- rewrite ---|
```
The rewrite range starts at the leading cv-qualifiers and ends at the
STAR of the pointer. other-specifiers before the leading cv-qualifiers
and after the STAR do not belong to the pointee type; they are
therefore left untouched, regardless of whether they are macros or
not. There should be nothing but trailing cv-qualifiers between T and
the STAR; those cv-qualifiers do belong to the pointee type, so they
are included in the rewrite range.
For arrays, let T mark the spelling of the element type according to
its TypeLoc:
```
other-specifiers cv-qualifiers T cv-qualifiers other-specifiers array-name [N]
|--------- rewrite ---------|
```
The rewrite range starts at the leading cv-qualifiers and ends at the
trailing cv-qualifiers. other-specifiers do not belong to the element
type, so if any token intervenes between T and the trailing
cv-qualifiers, the function fails to find a rewrite range and bails.
---
.../Transformations/CppBoundedBuffers.h | 30 +-
.../Transformations/CppBoundedBuffers.cpp | 397 ++++++++++++------
.../CppBoundedBuffersTest.cpp | 361 ++++++++++++++--
3 files changed, 599 insertions(+), 189 deletions(-)
diff --git a/clang/include/clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h b/clang/include/clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h
index 5b37282b313cf..73d3b56586a45 100644
--- a/clang/include/clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h
+++ b/clang/include/clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h
@@ -34,29 +34,35 @@ enum class BoundedType { Ptr, Array };
/// Why a reachable declarator was reported instead of rewritten.
enum class ReportReason {
+ ArrayNotEndInBracket,
+ DeclarationGroup,
+ EmissionFailed,
+ IncompleteArray,
+ MacroExpansion,
+ MultiDimensionalArray,
MultiLevelPointer,
+ NoInnerTypeLoc,
+ NotPointerTypeEndWithStar,
+ NotTransformed,
PointerToArray,
ReferenceToPointer,
- MultiDimensionalArray,
- IncompleteArray,
- UnreproducibleType,
- DeclarationGroup,
- MacroExpansion,
TrailingReturnType,
- EmissionFailed,
- NotTransformed,
+ UnexpectedLeadingQualifier,
+ UnexpectedTrailingQualifier,
+ UnnamableType,
};
/// Returns the report message for \p Reason.
llvm::StringRef messageFor(ReportReason Reason);
/// The outcome of classifying a declared type against the reachable pointer
-/// levels of its entity: a bounded-type rewrite, a report reason, or neither.
+/// levels of its entity: a bounded-type rewrite, or a report reason.
struct ClassifyResult {
- std::optional<BoundedType> NewType;
- // Pointee/element spelling; meaningful only when NewType is set.
+ // Meaningful only when Skip is nullopt.
+ BoundedType NewType = BoundedType::Ptr;
+ // Pointee/element spelling; meaningful only when Skip is nullopt.
std::string InnerSpelling;
- std::optional<ReportReason> Skip;
+ std::optional<ReportReason> Skip = ReportReason::NotTransformed;
};
/// Classifies the declared type \p T of a reachable entity. \p ReachableLevels
@@ -65,7 +71,7 @@ ClassifyResult
classifyDeclType(QualType T, const llvm::SmallSet<unsigned, 4> &ReachableLevels,
const ASTContext &Ctx);
-class CppBoundedBuffers : public Transformation {
+class CppBoundedBuffers final : public Transformation {
public:
using Transformation::Transformation;
diff --git a/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp b/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
index 25816420e504a..180d5e9f9c7d4 100644
--- a/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
+++ b/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
@@ -14,6 +14,7 @@
#include "clang/AST/DynamicRecursiveASTVisitor.h"
#include "clang/AST/Type.h"
#include "clang/AST/TypeLoc.h"
+#include "clang/Basic/LangOptions.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Lex/Lexer.h"
@@ -29,6 +30,7 @@
#include "llvm/ADT/SmallVector.h"
#include <cassert>
#include <map>
+#include <optional>
#include <string>
using namespace clang;
@@ -49,35 +51,24 @@ std::string spell(QualType T, const ASTContext &Ctx) {
return T.getAsString(Ctx.getPrintingPolicy());
}
-/// Whether \p T can be re-emitted as written. Anonymous records and lambdas
-/// have no usable spelling.
-bool isReproducible(QualType T) {
- const auto *RT = T->getAs<RecordType>();
- if (!RT)
- return true;
- const RecordDecl *RD = RT->getDecl();
- if (const auto *CXXRD = dyn_cast<CXXRecordDecl>(RD))
- if (CXXRD->isLambda())
- return false;
- return RD->getIdentifier() || RD->getTypedefNameForAnonDecl();
-}
-
-std::string cvPrefix(QualType T) {
- std::string Prefix;
- if (T.isLocalConstQualified())
- Prefix += "const ";
- if (T.isLocalVolatileQualified())
- Prefix += "volatile ";
- return Prefix;
+/// Whether \p T is a type with a name that can be used in template arguments.
+bool isNamable(QualType T) {
+ if (!T->isTypedefNameType())
+ if (const auto *RT = T->getAs<RecordType>()) {
+ const RecordDecl *RD = RT->getDecl();
+ return RD->getIdentifier() || RD->getTypedefNameForAnonDecl();
+ }
+ return true;
}
std::string renderNewType(const ClassifyResult &R, QualType T,
const ASTContext &Ctx) {
- if (*R.NewType == BoundedType::Ptr)
- return cvPrefix(T) + "bounded_ptr<" + R.InnerSpelling + "> ";
+ assert(!R.Skip);
+ if (R.NewType == BoundedType::Ptr)
+ return "bounded_ptr<" + R.InnerSpelling + "> ";
const auto *CAT = Ctx.getAsConstantArrayType(T);
std::string N = std::to_string(CAT->getSize().getZExtValue());
- return "bounded_array<" + R.InnerSpelling + ", " + N + "> ";
+ return "bounded_array<" + R.InnerSpelling + ", " + N + ">";
}
/// Whether another declarator in \p D's lexical context shares its type
@@ -110,25 +101,113 @@ CharSourceRange declTypeRange(const DeclaratorDecl *D) {
return CharSourceRange::getTokenRange(D->getSourceRange());
}
-/// A leading cv-qualifier keyword (e.g. the `const` in `const char *`) is not
-/// covered by the type-loc's begin location; extend \p TypeBegin left over it.
-SourceLocation extendOverLeadingQualifiers(SourceLocation TypeBegin,
- const ASTContext &Ctx) {
+/// \return the pointee or element types TypeLoc if TL is a (qualified) pointer
+/// or array type.
+TypeLoc getInnerTypeLoc(TypeLoc TL) {
+ TL = TL.getUnqualifiedLoc();
+ if (auto PTL = TL.getAs<PointerTypeLoc>())
+ return PTL.getPointeeLoc();
+ if (auto ATL = TL.getAs<ArrayTypeLoc>())
+ return ATL.getElementLoc();
+ return {};
+}
+
+/// Whether \p T spells a cv-qualifier keyword.
+bool isCVQualifier(const Token &T) {
+ return T.is(tok::raw_identifier) && (T.getRawIdentifier() == "const" ||
+ T.getRawIdentifier() == "volatile");
+}
+
+/// Probe leading qualifiers for a type 'T'. The probe is bounded in the range
+/// [ \p DeclBegin, \p TypeBegin ), where the lower bound is the begin location
+/// of the declaration where 'T' is spelled and the upper bound is the begin of
+/// the spell of 'T'.
+///
+/// The function updates \p TypeBegin if it finds cv-qualifiers preceding the
+/// original \p TypeBegin without any other token intervening in between. \p
+/// TypeBegin is not updated if there is no leading cv-qualifier. Otherwise,
+/// returns the probe failed reason.
+///
+/// \p TypeBegin is always token location.
+std::optional<ReportReason> extendLeadingQualifiers(SourceLocation DeclBegin,
+ SourceLocation &TypeBegin,
+ const ASTContext &Ctx) {
const SourceManager &SM = Ctx.getSourceManager();
const LangOptions &LangOpts = Ctx.getLangOpts();
- while (std::optional<Token> Prev = Lexer::findPreviousToken(
- TypeBegin, SM, LangOpts, /*IncludeComments=*/false)) {
- // findPreviousToken lexes raw tokens, so keywords arrive as identifiers.
- if (!Prev->is(tok::raw_identifier))
- break;
- StringRef Text = Prev->getRawIdentifier();
- if (Text != "const" && Text != "volatile")
- break;
- TypeBegin = Prev->getLocation();
+
+ std::optional<SourceLocation> FirstCVBegin;
+ std::optional<Token> Tok = Token();
+
+ if (Lexer::getRawToken(DeclBegin, *Tok, SM, LangOpts,
+ /*IgnoreWhiteSpace=*/true))
+ return ReportReason::EmissionFailed;
+ while (SM.isBeforeInTranslationUnit(Tok->getLocation(), TypeBegin)) {
+ if (isCVQualifier(*Tok)) {
+ if (!FirstCVBegin) {
+ // Found first cv-qualifier, set `FirstCVBegin`.
+ FirstCVBegin = Tok->getLocation();
+ }
+ } else if (FirstCVBegin)
+ // Bail when there is unexpected token between cv-qualifiers and the
+ // original TypeBegin:
+ return ReportReason::UnexpectedLeadingQualifier;
+ Tok = Lexer::findNextToken(Tok->getEndLoc(), SM, LangOpts,
+ /*IncludeComments=*/true);
+ if (!Tok)
+ return ReportReason::EmissionFailed;
+ }
+ if (FirstCVBegin)
+ TypeBegin = *FirstCVBegin; // set the real TypeBegin after propagation
+ return std::nullopt;
+}
+
+/// Probe trailing qualifiers for a type 'T'. The probe is bounded in the range
+/// ( \p TypeEnd, \p UpperBound ), where the lower bound is the end location
+/// of 'T' and the upper bound should be a location within the declaration where
+/// 'T' is spelled.
+///
+/// The function updates \p TypeEnd if it finds cv-qualifiers following the
+/// original \p TypeEnd without any other token intervening in between.
+/// \p TypeEnd is not updated if there is no following cv-qualifier. Otherwise,
+/// returns the probe failed reason.
+///
+/// \p TypeBegin is always token location.
+std::optional<ReportReason> extendTrailingQualifiers(SourceLocation &TypeEnd,
+ SourceLocation UpperBound,
+ const ASTContext &Ctx) {
+ const SourceManager &SM = Ctx.getSourceManager();
+ const LangOptions &LangOpts = Ctx.getLangOpts();
+
+ std::optional<SourceLocation> LastCVBegin;
+ bool RunEnded = false;
+
+ std::optional<Token> Tok = Lexer::findNextToken(TypeEnd, SM, LangOpts,
+ /*IncludeComments=*/true);
+ if (!Tok)
+ return ReportReason::EmissionFailed;
+ while (SM.isBeforeInTranslationUnit(Tok->getLocation(), UpperBound)) {
+ if (isCVQualifier(*Tok)) {
+ // Bail if there is anything unexpected between TypeEnd and a
+ // cv-qualifier.
+ if (RunEnded)
+ return ReportReason::UnexpectedTrailingQualifier;
+ LastCVBegin = Tok->getLocation();
+ } else
+ RunEnded = true;
+ Tok = Lexer::findNextToken(Tok->getEndLoc(), SM, LangOpts,
+ /*IncludeComments=*/true);
+ if (!Tok)
+ return ReportReason::EmissionFailed;
}
- return TypeBegin;
+ if (LastCVBegin)
+ TypeEnd = *LastCVBegin; // set the real TypeEnd after propagation
+ return std::nullopt;
}
+using Levels = llvm::SmallSet<unsigned, 4>;
+using DeclLevels = std::map<const Decl *, Levels>;
+using ReturnLevels = std::map<const FunctionDecl *, Levels>;
+
/// Reverse index from the whole-program reachability result onto entity names,
/// so a declaration in this TU can look up its reachable pointer levels.
class ReachabilityMap {
@@ -160,14 +239,6 @@ class ReachabilityMap {
}
};
-struct Candidate {
- llvm::SmallSet<unsigned, 4> Levels;
- bool AccountedFor = false;
-};
-
-using DeclLevels = std::map<const Decl *, Candidate>;
-using ReturnLevels = std::map<const FunctionDecl *, Candidate>;
-
/// Collects the reachable pointer/array declarators and function returns
/// declared in this TU.
class CollectVisitor : public DynamicRecursiveASTVisitor {
@@ -191,7 +262,7 @@ class CollectVisitor : public DynamicRecursiveASTVisitor {
llvm::SmallSet<unsigned, 4> Levels =
Reach.levelsFor(getEntityNameForReturn(FD));
if (!Levels.empty())
- Returns[FD].Levels = std::move(Levels);
+ Returns[FD] = std::move(Levels);
}
return true;
}
@@ -202,7 +273,7 @@ class CollectVisitor : public DynamicRecursiveASTVisitor {
return;
llvm::SmallSet<unsigned, 4> Levels = Reach.levelsFor(Name);
if (!Levels.empty())
- Decls[D].Levels = std::move(Levels);
+ Decls[D] = std::move(Levels);
}
const ReachabilityMap &Reach;
@@ -232,26 +303,23 @@ class RewriteVisitor : public DynamicRecursiveASTVisitor {
auto It = Returns.find(FD);
if (It == Returns.end())
return true;
- Candidate &Cand = It->second;
+ const Levels &ReachableLevels = It->second;
if (hasTrailingReturnType(FD))
- return account(Cand, FD, ReportReason::TrailingReturnType);
+ return report(FD, ReportReason::TrailingReturnType);
- SourceLocation TypeBegin = FD->getReturnTypeSourceRange().getBegin();
SourceLocation NameLoc = FD->getLocation();
- if (TypeBegin.isMacroID() || NameLoc.isMacroID())
- return account(Cand, FD, ReportReason::MacroExpansion);
- ClassifyResult R = classifyDeclType(FD->getReturnType(), Cand.Levels, Ctx);
+ ClassifyResult R =
+ classifyDeclType(FD->getReturnType(), ReachableLevels, Ctx);
if (R.Skip)
- return account(Cand, FD, *R.Skip);
- if (R.NewType) {
- bool Ok = emit(TypeBegin, NameLoc, FD->getReturnType(), R,
- /*ArrayTypeLoc=*/std::nullopt);
- return account(Cand, FD,
- Ok ? std::nullopt
- : std::optional(ReportReason::EmissionFailed));
- }
- return true;
+ return report(FD, *R.Skip);
+
+ FunctionTypeLoc FunTypeLoc = FD->getFunctionTypeLoc();
+
+ if (!FunTypeLoc)
+ return report(FD, ReportReason::EmissionFailed);
+ return report(FD, emit(FD->getBeginLoc(), NameLoc,
+ FunTypeLoc.getReturnLoc(), FD->getReturnType(), R));
}
private:
@@ -259,77 +327,139 @@ class RewriteVisitor : public DynamicRecursiveASTVisitor {
auto It = Decls.find(D);
if (It == Decls.end())
return;
- Candidate &Cand = It->second;
+ const Levels &ReachableLevels = It->second;
if (sharesTypeSpecifier(D))
- return (void)account(Cand, D, ReportReason::DeclarationGroup);
+ return (void)report(D, ReportReason::DeclarationGroup);
const TypeSourceInfo *TSI = D->getTypeSourceInfo();
- SourceLocation TypeBegin =
- TSI ? TSI->getTypeLoc().getBeginLoc() : SourceLocation();
+
+ if (!TSI)
+ return (void)report(D, ReportReason::EmissionFailed);
+
SourceLocation NameLoc = D->getLocation();
- if (TypeBegin.isMacroID() || NameLoc.isMacroID())
- return (void)account(Cand, D, ReportReason::MacroExpansion);
+ ClassifyResult R = classifyDeclType(T, ReachableLevels, Ctx);
- ClassifyResult R = classifyDeclType(T, Cand.Levels, Ctx);
if (R.Skip)
- return (void)account(Cand, D, *R.Skip);
- if (R.NewType) {
- std::optional<TypeLoc> ArrayTypeLoc;
- if (*R.NewType == BoundedType::Array && TSI)
- ArrayTypeLoc = TSI->getTypeLoc();
- bool Ok = emit(TypeBegin, NameLoc, T, R, ArrayTypeLoc);
- account(Cand, D,
- Ok ? std::nullopt : std::optional(ReportReason::EmissionFailed));
+ return (void)report(D, *R.Skip);
+ report(D, emit(D->getBeginLoc(), NameLoc, TSI->getTypeLoc(), T, R));
+ }
+
+ /// Compute the precise source range for rewriting. The produced range is
+ /// token range.
+ ///
+ /// For pointer types, the rewrite range is from the leading cv-qualifier of
+ /// the pointee type to the '*' token of the pointer type.
+ ///
+ /// For array types, the rewrite range is from the leading cv-qualifier to the
+ /// trailing cv-qualifier around the element type. It stops short of the
+ /// declarator name, leaving the name and the extent that follows it to be
+ /// handled separately.
+ ///
+ /// \param DeclBegin the begin location of the declaration, the lower bound of
+ /// the source range before narrowing down to the precise one.
+ /// \param NameLoc the location of the name of the declaration, the upper
+ /// bound of the source range before narrowing down to the precise one.
+ /// \param TLoc the TypeLoc of the type of the declaration
+ /// \param BoundedType indicates whether it is a pointer or an array
+ /// \return ReportReason if it cannot narrow down the rewrite range to the
+ /// aforementioned range. std::nullopt and updated \p Result otherwise.
+ std::optional<ReportReason>
+ computeRewriteRange(SourceLocation DeclBegin, SourceLocation NameLoc,
+ TypeLoc TLoc, BoundedType BoundedType,
+ const ASTContext &Ctx, SourceRange &RewriteRange) {
+ TypeLoc InnerTypeLoc = getInnerTypeLoc(TLoc);
+
+ if (!InnerTypeLoc)
+ return ReportReason::NoInnerTypeLoc;
+
+ SourceLocation RewriteRangeBegin = InnerTypeLoc.getBeginLoc();
+ SourceRange Result;
+
+ if (BoundedType == BoundedType::Ptr) {
+ auto PTL = TLoc.getUnqualifiedLoc().getAs<PointerTypeLoc>();
+
+ if (!PTL || TLoc.getEndLoc() != PTL.getStarLoc())
+ return ReportReason::NotPointerTypeEndWithStar;
+ if (auto Reason =
+ extendLeadingQualifiers(DeclBegin, RewriteRangeBegin, Ctx))
+ return Reason;
+ Result = {RewriteRangeBegin, PTL.getStarLoc()};
+ } else {
+ SourceLocation RewriteRangeEnd = InnerTypeLoc.getEndLoc();
+
+ if (auto Reason =
+ extendLeadingQualifiers(DeclBegin, RewriteRangeBegin, Ctx))
+ return Reason;
+ if (auto Reason = extendTrailingQualifiers(RewriteRangeEnd, NameLoc, Ctx))
+ return Reason;
+ Result = {RewriteRangeBegin, RewriteRangeEnd};
}
+
+ if (Result.getBegin().isMacroID() || Result.getEnd().isMacroID())
+ return ReportReason::MacroExpansion;
+ if (Result.getBegin().isInvalid() || Result.getEnd().isInvalid())
+ return ReportReason::EmissionFailed;
+
+ const SourceManager &SM = Ctx.getSourceManager();
+ if (SM.getFileID(Result.getBegin()) != SM.getFileID(Result.getEnd()))
+ return ReportReason::EmissionFailed;
+ RewriteRange = Result;
+ return std::nullopt;
}
/// Emits the type-token replacement (and, for arrays, deletes the trailing
/// extent). Returns false without emitting anything if a valid,
/// self-contained edit cannot be formed.
- bool emit(SourceLocation TypeBegin, SourceLocation NameLoc, QualType T,
- const ClassifyResult &R, std::optional<TypeLoc> ForArray) {
+ std::optional<ReportReason> emit(SourceLocation DeclBegin,
+ SourceLocation NameLoc, TypeLoc TLoc,
+ QualType T, const ClassifyResult &R) {
const SourceManager &SM = Ctx.getSourceManager();
- if (TypeBegin.isValid() && !TypeBegin.isMacroID())
- TypeBegin = extendOverLeadingQualifiers(TypeBegin, Ctx);
- if (TypeBegin.isInvalid() || NameLoc.isInvalid() || TypeBegin.isMacroID() ||
- NameLoc.isMacroID() ||
- SM.getFileID(TypeBegin) != SM.getFileID(NameLoc) ||
- SM.getFileOffset(NameLoc) <= SM.getFileOffset(TypeBegin))
- return false;
+ SourceRange TypeRewriteRange;
+
+ if (auto Reason = computeRewriteRange(DeclBegin, NameLoc, TLoc, R.NewType,
+ Ctx, TypeRewriteRange))
+ return Reason;
+ // TypeRewriteRange is bounded by the tokens (begin location) of the two
+ // ends. Now convert it to char range for source edit, which requires the
+ // bounds to be the characters of the two ends.
+ CharSourceRange TypeRewriteCharRange =
+ Lexer::getAsCharRange(TypeRewriteRange, SM, Ctx.getLangOpts());
llvm::SmallVector<tooling::Replacement, 2> Edited;
- Edited.emplace_back(SM, CharSourceRange::getCharRange(TypeBegin, NameLoc),
- renderNewType(R, T, Ctx), Ctx.getLangOpts());
- if (ForArray) {
- ArrayTypeLoc ATL = ForArray->getAs<ArrayTypeLoc>();
+ Edited.emplace_back(SM, TypeRewriteCharRange, renderNewType(R, T, Ctx),
+ Ctx.getLangOpts());
+
+ if (R.NewType == BoundedType::Array) {
+ ArrayTypeLoc ATL = TLoc.getUnqualifiedLoc().getAs<ArrayTypeLoc>();
+
if (!ATL)
- return false;
+ return ReportReason::EmissionFailed;
+
SourceLocation LBracket = ATL.getLBracketLoc();
SourceLocation RBracket = ATL.getRBracketLoc();
// A clean array declarator ends at its closing bracket; otherwise the
// element spelling wraps the name (e.g. an array of function pointers)
// and cannot be rewritten by stripping a trailing extent.
- if (LBracket.isInvalid() || RBracket.isInvalid() ||
- ForArray->getEndLoc() != RBracket)
- return false;
+ if (ATL.getEndLoc() != RBracket)
+ return ReportReason::ArrayNotEndInBracket;
+ if (LBracket.isInvalid() || RBracket.isInvalid())
+ return ReportReason::EmissionFailed;
Edited.emplace_back(SM,
CharSourceRange::getTokenRange(LBracket, RBracket),
"", Ctx.getLangOpts());
}
- for (const tooling::Replacement &Repl : Edited)
- if (!Repl.isApplicable())
- return false;
+ if (!llvm::all_of(Edited, std::mem_fn(&tooling::Replacement::isApplicable)))
+ return ReportReason::EmissionFailed;
for (tooling::Replacement &Repl : Edited)
Edits.addReplacement(std::move(Repl));
- return true;
+ return std::nullopt;
}
- /// Marks \p Cand accounted for, reporting \p Reason if one is given.
- bool account(Candidate &Cand, const DeclaratorDecl *D,
- std::optional<ReportReason> Reason) {
- Cand.AccountedFor = true;
+ /// Reports \p Reason for \p D, if one is given. Always returns true so that
+ /// visitors can tail-call it.
+ bool report(const DeclaratorDecl *D, std::optional<ReportReason> Reason) {
if (Reason)
Report.addResult(SkippedRuleId, SarifResultLevel::Note, declTypeRange(D),
messageFor(*Reason));
@@ -349,28 +479,39 @@ namespace clang::ssaf {
llvm::StringRef messageFor(ReportReason Reason) {
switch (Reason) {
+ case ReportReason::ArrayNotEndInBracket:
+ return "the array type does not end in a closing bracket";
+ case ReportReason::DeclarationGroup:
+ return "declarator of a multi-declarator group is not yet rewritten";
+ case ReportReason::EmissionFailed:
+ return "no source edit could be formed for this declarator";
+ case ReportReason::IncompleteArray:
+ return "array of unknown bound is not yet rewritten";
+ case ReportReason::MacroExpansion:
+ return "declarator spelled through a macro is not yet rewritten";
+ case ReportReason::MultiDimensionalArray:
+ return "multi-dimensional array is not yet rewritten";
case ReportReason::MultiLevelPointer:
return "multi-level pointer indirection is not yet rewritten";
+ case ReportReason::NoInnerTypeLoc:
+ return "no TypeLoc for the pointee or array element type";
+ case ReportReason::NotPointerTypeEndWithStar:
+ return "pointer declarator does not end at its '*'";
+ case ReportReason::NotTransformed:
+ return "this declaration was not transformed";
case ReportReason::PointerToArray:
return "pointer to array is not yet rewritten";
case ReportReason::ReferenceToPointer:
return "reference to pointer is not yet rewritten";
- case ReportReason::MultiDimensionalArray:
- return "multi-dimensional array is not yet rewritten";
- case ReportReason::IncompleteArray:
- return "array of unknown bound is not yet rewritten";
- case ReportReason::UnreproducibleType:
- return "type spelling cannot be reproduced";
- case ReportReason::DeclarationGroup:
- return "declarator of a multi-declarator group is not yet rewritten";
- case ReportReason::MacroExpansion:
- return "declarator spelled through a macro is not yet rewritten";
case ReportReason::TrailingReturnType:
return "trailing return type is not yet rewritten";
- case ReportReason::EmissionFailed:
- return "no source edit could be formed for this declarator";
- case ReportReason::NotTransformed:
- return "reachable buffer was not transformed";
+ case ReportReason::UnexpectedLeadingQualifier:
+ return "unexpected token between a leading cv-qualifier and the type";
+ case ReportReason::UnexpectedTrailingQualifier:
+ return "unexpected token between the type and a trailing cv-qualifier";
+ case ReportReason::UnnamableType:
+ return "the pointee or array element type has no name that can be written "
+ "as a template argument";
}
llvm_unreachable("unhandled ReportReason");
}
@@ -411,12 +552,13 @@ classifyDeclType(QualType T, const llvm::SmallSet<unsigned, 4> &ReachableLevels,
R.Skip = ReportReason::PointerToArray;
return R;
}
- if (!isReproducible(Pointee)) {
- R.Skip = ReportReason::UnreproducibleType;
+ if (!isNamable(Pointee)) {
+ R.Skip = ReportReason::UnnamableType;
return R;
}
R.NewType = BoundedType::Ptr;
R.InnerSpelling = Pointee->isVoidType() ? "char" : spell(Pointee, Ctx);
+ R.Skip = std::nullopt;
return R;
}
@@ -426,12 +568,13 @@ classifyDeclType(QualType T, const llvm::SmallSet<unsigned, 4> &ReachableLevels,
R.Skip = ReportReason::MultiDimensionalArray;
return R;
}
- if (!isReproducible(Element)) {
- R.Skip = ReportReason::UnreproducibleType;
+ if (!isNamable(Element)) {
+ R.Skip = ReportReason::UnnamableType;
return R;
}
R.NewType = BoundedType::Array;
R.InnerSpelling = spell(Element, Ctx);
+ R.Skip = std::nullopt;
return R;
}
@@ -454,20 +597,6 @@ void CppBoundedBuffers::HandleTranslationUnit(ASTContext &Ctx) {
Decl *TU = Ctx.getTranslationUnitDecl();
CollectVisitor(Reach, Decls, Returns).TraverseDecl(TU);
RewriteVisitor(Ctx, Decls, Returns, Edits, Report).TraverseDecl(TU);
-
- // Every reachable buffer in this TU is either rewritten or reported; a
- // leftover means it was neither, which must still be surfaced.
- for (const auto &[D, Cand] : Decls)
- if (!Cand.AccountedFor)
- Report.addResult(SkippedRuleId, SarifResultLevel::Note,
- declTypeRange(cast<DeclaratorDecl>(D)),
- messageFor(ReportReason::NotTransformed));
- for (const auto &[FD, Cand] : Returns)
- if (!Cand.AccountedFor)
- Report.addResult(
- SkippedRuleId, SarifResultLevel::Note,
- CharSourceRange::getTokenRange(FD->getReturnTypeSourceRange()),
- messageFor(ReportReason::NotTransformed));
}
} // namespace clang::ssaf
diff --git a/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp b/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
index ca6e5e3894e70..2bc5dfda48852 100644
--- a/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
@@ -25,6 +25,7 @@
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/SmallSet.h"
+#include "llvm/ADT/StringSet.h"
#include "llvm/Support/Error.h"
#include "gtest/gtest.h"
#include <memory>
@@ -107,7 +108,8 @@ class CppBoundedBuffersTest : public TestFixture {
// Parses \p Code, lets \p Mark populate the reachable result, runs the
// transformation, and returns the rewritten source and report entries.
Captured runMarked(StringRef Code, MarkFn Mark) {
- std::unique_ptr<ASTUnit> AST = tooling::buildASTFromCode(Code);
+ std::unique_ptr<ASTUnit> AST =
+ tooling::buildASTFromCodeWithArgs(Code, {"-std=c++20"});
ASTContext &Ctx = AST->getASTContext();
WPASuite Suite = makeWPASuite();
@@ -142,7 +144,7 @@ class CppBoundedBuffersTest : public TestFixture {
TEST_F(CppBoundedBuffersTest, PointerLocal) {
Captured C = run("void f() { int *p; }",
[](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "void f() { bounded_ptr<int> p; }");
+ ASSERT_TRUE(C.Rewritten == "void f() { bounded_ptr<int> p; }");
EXPECT_TRUE(C.Reports.empty());
}
@@ -150,28 +152,28 @@ TEST_F(CppBoundedBuffersTest, PointerParameter) {
Captured C =
run("void f(int *p);",
[](ASTContext &Ctx) { return paramEntity("f", 0, Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "void f(bounded_ptr<int> p);");
+ ASSERT_TRUE(C.Rewritten == "void f(bounded_ptr<int> p);");
EXPECT_TRUE(C.Reports.empty());
}
TEST_F(CppBoundedBuffersTest, ConstQualifiedPointee) {
Captured C = run("const char *s;",
[](ASTContext &Ctx) { return varEntity("s", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "bounded_ptr<const char> s;");
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<const char> s;");
EXPECT_TRUE(C.Reports.empty());
}
TEST_F(CppBoundedBuffersTest, VoidPointer) {
Captured C =
run("void *p;", [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "bounded_ptr<char> p;");
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<char> p;");
EXPECT_TRUE(C.Reports.empty());
}
TEST_F(CppBoundedBuffersTest, ArrayField) {
Captured C = run("struct S { int a[10]; };",
[](ASTContext &Ctx) { return fieldEntity("a", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "struct S { bounded_array<int, 10> a; };");
+ ASSERT_TRUE(C.Rewritten == "struct S { bounded_array<int, 10> a; };");
EXPECT_TRUE(C.Reports.empty());
}
@@ -179,28 +181,28 @@ TEST_F(CppBoundedBuffersTest, FunctionReturn) {
Captured C =
run("int *foo();",
[](ASTContext &Ctx) { return returnEntity("foo", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "bounded_ptr<int> foo();");
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<int> foo();");
EXPECT_TRUE(C.Reports.empty());
}
TEST_F(CppBoundedBuffersTest, GlobalPointer) {
Captured C =
run("int *g;", [](ASTContext &Ctx) { return varEntity("g", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "bounded_ptr<int> g;");
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<int> g;");
EXPECT_TRUE(C.Reports.empty());
}
TEST_F(CppBoundedBuffersTest, PointerField) {
Captured C = run("struct S { int *p; };",
[](ASTContext &Ctx) { return fieldEntity("p", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "struct S { bounded_ptr<int> p; };");
+ ASSERT_TRUE(C.Rewritten == "struct S { bounded_ptr<int> p; };");
EXPECT_TRUE(C.Reports.empty());
}
TEST_F(CppBoundedBuffersTest, ArrayOfPointers) {
Captured C = run("int *a[10];",
[](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "bounded_array<int *, 10> a;");
+ ASSERT_TRUE(C.Rewritten == "bounded_array<int *, 10> a;");
EXPECT_TRUE(C.Reports.empty());
}
@@ -209,7 +211,144 @@ TEST_F(CppBoundedBuffersTest, ArrayOfFunctionPointers) {
// pointer); the typedef keeps the declarator a clean prefix + [N] suffix.
Captured C = run("typedef void (*FP)(); FP fps[4];",
[](ASTContext &Ctx) { return varEntity("fps", Ctx); }, {1});
- EXPECT_EQ(C.Rewritten, "typedef void (*FP)(); bounded_array<FP, 4> fps;");
+ ASSERT_TRUE(C.Rewritten == "typedef void (*FP)(); bounded_array<FP, 4> fps;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ConstQualifiedPointeeSpelledAfter) {
+ // `char const *` means the same as `const char *`; the qualifier belongs to
+ // the pointee either way and is reproduced inside the angle brackets.
+ Captured C = run("char const *s;",
+ [](ASTContext &Ctx) { return varEntity("s", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<const char> s;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ConstVolatileQualifiedPointee) {
+ Captured C = run("const volatile char *s;",
+ [](ASTContext &Ctx) { return varEntity("s", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<const volatile char> s;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ConstPointerKeepsItsOwnQualifier) {
+ // The `const` applies to the pointer, not the pointee, so it lies outside the
+ // rewrite range and stays where it was written.
+ Captured C = run("int *const p = nullptr;",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<int> const p = nullptr;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ConstPointerToConstPointee) {
+ Captured C = run("const int *const p = nullptr;",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<const int> const p = nullptr;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, StorageClassBeforeQualifiedPointee) {
+ // `static` precedes the qualifier run and is left untouched.
+ Captured C = run("static const char *s;",
+ [](ASTContext &Ctx) { return varEntity("s", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "static bounded_ptr<const char> s;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ConstQualifiedArrayElement) {
+ Captured C = run("const int a[10] = {};",
+ [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_array<const int, 10> a = {};");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, ConstQualifiedArrayElementSpelledAfter) {
+ Captured C = run("int const a[10] = {};",
+ [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_array<const int, 10> a = {};");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, QualifiedPointerFunctionReturn) {
+ Captured C =
+ run("const char *foo();",
+ [](ASTContext &Ctx) { return returnEntity("foo", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<const char> foo();");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, MultipleTrailingPointerQualifiers) {
+ Captured C = run("int *volatile const p = nullptr;",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<int> volatile const p = nullptr;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, LeadingAndMultipleTrailingPointerQualifiers) {
+ Captured C = run("const int *const volatile p = nullptr;",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten ==
+ "bounded_ptr<const int> const volatile p = nullptr;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, MultipleTrailingArrayQualifiers) {
+ // Both qualify the element, so the range grows right over the whole run.
+ Captured C = run("int const volatile a[10] = {};",
+ [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_array<const volatile int, 10> a = {};");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, MultipleTrailingArrayQualifiersReversed) {
+ Captured C = run("int volatile const a[10] = {};",
+ [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_array<const volatile int, 10> a = {};");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CommentBetweenPointeeTypeAndStar) {
+ Captured C = run("int /* c */ *p;",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<int> p;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, AttributeBetweenPointeeTypeAndStar) {
+ // A type attribute also sits inside the rewrite range. It is part of the
+ // pointee type, so the pretty-printed spelling reproduces it.
+ Captured C = run("int __attribute__((address_space(1))) *p;",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten,
+ "bounded_ptr<__attribute__((address_space(1))) int> p;");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, DeclAttributeAfterArrayBrackets) {
+ // A declaration attribute is not part of the type-loc, so it lies beyond the
+ // deleted extent and survives untouched.
+ Captured C = run("int a[10] __attribute__((aligned(16)));",
+ [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten,
+ "bounded_array<int, 10> a __attribute__((aligned(16)));");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, DeclAttributeAfterPointerDeclarator) {
+ Captured C = run("int *p __attribute__((aligned(16)));",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ ASSERT_TRUE(C.Rewritten ==
+ "bounded_ptr<int> p __attribute__((aligned(16)));");
+ EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, AliasedLambdaPointee) {
+ // The closure type itself is unnamed, but the alias supplies a name that can
+ // be written as the template argument, and it denotes that same closure type.
+ Captured C = run("using L = decltype([](int x) { return x; });\nL *p;\n",
+ [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ EXPECT_EQ(C.Rewritten, "using L = decltype([](int x) { return x; });\n"
+ "bounded_ptr<L> p;\n");
EXPECT_TRUE(C.Reports.empty());
}
@@ -218,10 +357,10 @@ TEST_F(CppBoundedBuffersTest, ArrayOfFunctionPointers) {
//===----------------------------------------------------------------------===//
void expectSkip(const Captured &C, StringRef Original, ReportReason Reason) {
- EXPECT_EQ(C.Rewritten, Original);
- ASSERT_EQ(C.Reports.size(), 1u);
- EXPECT_EQ(C.Reports[0].Level, SarifResultLevel::Note);
- EXPECT_EQ(C.Reports[0].Message, messageFor(Reason).str());
+ ASSERT_TRUE(C.Rewritten == Original);
+ ASSERT_TRUE(C.Reports.size() == 1u);
+ ASSERT_TRUE(C.Reports[0].Level == SarifResultLevel::Note);
+ ASSERT_TRUE(C.Reports[0].Message == messageFor(Reason).str());
}
TEST_F(CppBoundedBuffersTest, MultiLevelPointer) {
@@ -259,11 +398,30 @@ TEST_F(CppBoundedBuffersTest, ReferenceToPointer) {
expectSkip(C, Code, ReportReason::ReferenceToPointer);
}
-TEST_F(CppBoundedBuffersTest, UnreproducibleType) {
+TEST_F(CppBoundedBuffersTest, UnnamableType) {
StringRef Code = "struct { int x; } *p;";
Captured C =
run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
- expectSkip(C, Code, ReportReason::UnreproducibleType);
+ expectSkip(C, Code, ReportReason::UnnamableType);
+}
+
+TEST_F(CppBoundedBuffersTest, InlineLambdaPointee) {
+ // Each lambda-expression yields a distinct closure type, so there is no name
+ // to write: re-spelling the expression would denote a different type.
+ StringRef Code = "decltype([](int x) { return x; }) *p;";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::UnnamableType);
+}
+
+TEST_F(CppBoundedBuffersTest, UnaliasedLambdaPointee) {
+ // decltype of a variable names the closure type but is not a typedef-name, so
+ // the unnamed record is what the check sees.
+ StringRef Code =
+ "void f() { auto lam = [](int x) { return x; }; decltype(lam) *p; }";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::UnnamableType);
}
TEST_F(CppBoundedBuffersTest, DeclarationGroup) {
@@ -275,11 +433,70 @@ TEST_F(CppBoundedBuffersTest, DeclarationGroup) {
markReachable(Suite, Result, varEntity("p", Ctx), {1});
markReachable(Suite, Result, varEntity("q", Ctx), {1});
});
- EXPECT_EQ(C.Rewritten, "int *p, *q;");
- ASSERT_EQ(C.Reports.size(), 2u);
+ ASSERT_TRUE(C.Rewritten == "int *p, *q;");
+ ASSERT_TRUE(C.Reports.size() == 2u);
+ for (const auto &R : C.Reports) {
+ ASSERT_TRUE(R.Level == SarifResultLevel::Note);
+ ASSERT_TRUE(R.Message == messageFor(ReportReason::DeclarationGroup).str());
+ }
+}
+
+TEST_F(CppBoundedBuffersTest, GlobalDeclarationGroupOfThree) {
+ // A three-way (not just two-way) comma group at namespace scope; every
+ // declarator, including the multi-level pointer, is reported.
+ StringRef Code = "extern int *const p, *const q, *volatile *pp;";
+ Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+ UnsafeBufferReachableAnalysisResult &Result) {
+ markReachable(Suite, Result, varEntity("p", Ctx), {1});
+ markReachable(Suite, Result, varEntity("q", Ctx), {1});
+ markReachable(Suite, Result, varEntity("pp", Ctx), {1, 2});
+ });
+ ASSERT_TRUE(C.Rewritten == Code);
+ ASSERT_TRUE(C.Reports.size() == 3u);
for (const auto &R : C.Reports) {
- EXPECT_EQ(R.Level, SarifResultLevel::Note);
- EXPECT_EQ(R.Message, messageFor(ReportReason::DeclarationGroup).str());
+ ASSERT_TRUE(R.Level == SarifResultLevel::Note);
+ ASSERT_TRUE(R.Message == messageFor(ReportReason::DeclarationGroup).str());
+ }
+}
+
+TEST_F(CppBoundedBuffersTest, FieldDeclarationGroupOfThree) {
+ // Same comma group, but as FieldDecls inside a RecordDecl rather than
+ // VarDecls inside the TranslationUnitDecl.
+ StringRef Code = "struct Tup { int *const p, *const q, *volatile *pp; };";
+ Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+ UnsafeBufferReachableAnalysisResult &Result) {
+ markReachable(Suite, Result, fieldEntity("p", Ctx), {1});
+ markReachable(Suite, Result, fieldEntity("q", Ctx), {1});
+ markReachable(Suite, Result, fieldEntity("pp", Ctx), {1, 2});
+ });
+ ASSERT_TRUE(C.Rewritten == Code);
+ ASSERT_TRUE(C.Reports.size() == 3u);
+ for (const auto &R : C.Reports) {
+ ASSERT_TRUE(R.Level == SarifResultLevel::Note);
+ ASSERT_TRUE(R.Message == messageFor(ReportReason::DeclarationGroup).str());
+ }
+}
+
+TEST_F(CppBoundedBuffersTest, ForInitDeclarationGroupOfThree) {
+ // Same comma group again, but as a DeclStmt in a for-loop init-statement;
+ // the lexical DeclContext is the enclosing function, not the loop itself.
+ StringRef Code = "void test() {\n"
+ " for (int *const p = {}, *const q = {}, "
+ "*volatile *pp = {}; true;) {\n"
+ " return;\n"
+ " }\n"
+ "}\n";
+ Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+ UnsafeBufferReachableAnalysisResult &Result) {
+ markReachable(Suite, Result, varEntity("p", Ctx), {1});
+ markReachable(Suite, Result, varEntity("q", Ctx), {1});
+ markReachable(Suite, Result, varEntity("pp", Ctx), {1, 2});
+ });
+ ASSERT_TRUE(C.Rewritten == Code);
+ ASSERT_TRUE(C.Reports.size() == 3u);
+ for (const auto &R : C.Reports) {
+ ASSERT_TRUE(R.Level == SarifResultLevel::Note);
+ ASSERT_TRUE(R.Message == messageFor(ReportReason::DeclarationGroup).str());
}
}
@@ -297,13 +514,61 @@ TEST_F(CppBoundedBuffersTest, TrailingReturnType) {
expectSkip(C, Code, ReportReason::TrailingReturnType);
}
-TEST_F(CppBoundedBuffersTest, EmissionFailureOnRawFunctionPointerArray) {
- // A raw array-of-function-pointers has no clean prefix + [N] suffix, so the
- // edit cannot be formed and the entity is reported rather than mangled.
+TEST_F(CppBoundedBuffersTest, RawFunctionPointerArrayDoesNotEndInBracket) {
+ // A raw array-of-function-pointers has no clean prefix + [N] suffix: the
+ // element spelling wraps the name, so the array type ends at the trailing
+ // `()` rather than at its closing bracket.
StringRef Code = "void (*fps[4])();";
Captured C =
run(Code, [](ASTContext &Ctx) { return varEntity("fps", Ctx); }, {1});
- expectSkip(C, Code, ReportReason::EmissionFailed);
+ expectSkip(C, Code, ReportReason::ArrayNotEndInBracket);
+}
+
+TEST_F(CppBoundedBuffersTest, ParenthesizedPointerDeclarator) {
+ // The parens wrap the declarator, so the type ends at the ')' rather than at
+ // the '*'. A range anchored on the type would span the name and unbalance the
+ // parens, so the declarator is reported instead.
+ StringRef Code = "int (*par);";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("par", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::NotPointerTypeEndWithStar);
+}
+
+TEST_F(CppBoundedBuffersTest, TypedefSpelledPointer) {
+ // The declarator spells no pointer of its own, so there is no pointee
+ // type-loc to build a rewrite range from.
+ StringRef Code = "typedef int *IP;\nIP p;\n";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::NoInnerTypeLoc);
+}
+
+TEST_F(CppBoundedBuffersTest, QualifierSeparatedFromPointeeType) {
+ // `const` is separated from the type by `static`, so absorbing it into the
+ // rewrite range would need a non-contiguous edit.
+ StringRef Code = "const static char *s;";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("s", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::UnexpectedLeadingQualifier);
+}
+
+TEST_F(CppBoundedBuffersTest, CommentBetweenQualifierAndPointeeType) {
+ // A comment interrupts the qualifier run; absorbing the `const` would delete
+ // the comment along with it.
+ StringRef Code = "const /* c */ char *s;";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("s", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::UnexpectedLeadingQualifier);
+}
+
+TEST_F(CppBoundedBuffersTest, CommentBetweenElementTypeAndTrailingQualifier) {
+ // The `const` qualifies the element, so its meaning moves inside the bounded
+ // type and it must be absorbed by the rewrite range. The comment separates it
+ // from the element type, which would need a non-contiguous edit.
+ StringRef Code = "int /* c */ const a[10] = {};";
+ Captured C =
+ run(Code, [](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
+ expectSkip(C, Code, ReportReason::UnexpectedTrailingQualifier);
}
//===----------------------------------------------------------------------===//
@@ -323,7 +588,7 @@ TEST_F(CppBoundedBuffersTest, NotReachable) {
StringRef Code = "int *p;";
Captured C =
run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {});
- EXPECT_EQ(C.Rewritten, Code);
+ ASSERT_TRUE(C.Rewritten == Code);
EXPECT_TRUE(C.Reports.empty());
}
@@ -336,8 +601,8 @@ TEST_F(CppBoundedBuffersTest, RewriteAndReportCoexist) {
markReachable(Suite, Result, varEntity("good", Ctx), {1});
markReachable(Suite, Result, varEntity("bad", Ctx), {1});
});
- EXPECT_EQ(C.Rewritten, "bounded_ptr<int> good; int **bad;");
- ASSERT_EQ(C.Reports.size(), 1u);
+ ASSERT_TRUE(C.Rewritten == "bounded_ptr<int> good; int **bad;");
+ ASSERT_TRUE(C.Reports.size() == 1u);
EXPECT_EQ(C.Reports[0].Message,
messageFor(ReportReason::MultiLevelPointer).str());
}
@@ -356,20 +621,21 @@ TEST_F(CppBoundedBuffersTest, ClassifyRewritesOutermostReachablePointer) {
Levels.insert(1);
ClassifyResult R = classifyDeclType(typeOf("p", AST->getASTContext()), Levels,
AST->getASTContext());
- ASSERT_TRUE(R.NewType.has_value());
- EXPECT_EQ(*R.NewType, BoundedType::Ptr);
- EXPECT_EQ(R.InnerSpelling, "int");
- EXPECT_FALSE(R.Skip.has_value());
+ ASSERT_FALSE(R.Skip.has_value());
+ ASSERT_TRUE(R.NewType == BoundedType::Ptr);
+ ASSERT_TRUE(R.InnerSpelling == "int");
}
TEST_F(CppBoundedBuffersTest, ClassifyIgnoresInnerOnlyReachablePointer) {
+ // A single pointer has only level 1, so nothing is recognized and the
+ // catch-all reason is reported rather than leaving the entity undecided.
auto AST = tooling::buildASTFromCode("int *p;");
llvm::SmallSet<unsigned, 4> Levels;
Levels.insert(2);
ClassifyResult R = classifyDeclType(typeOf("p", AST->getASTContext()), Levels,
AST->getASTContext());
- EXPECT_FALSE(R.NewType.has_value());
- EXPECT_FALSE(R.Skip.has_value());
+ ASSERT_TRUE(R.Skip.has_value());
+ ASSERT_TRUE(*R.Skip == ReportReason::NotTransformed);
}
TEST_F(CppBoundedBuffersTest, ClassifyMultiLevelPointerIsSkipped) {
@@ -378,20 +644,29 @@ TEST_F(CppBoundedBuffersTest, ClassifyMultiLevelPointerIsSkipped) {
Levels.insert(1);
ClassifyResult R = classifyDeclType(typeOf("pp", AST->getASTContext()),
Levels, AST->getASTContext());
- EXPECT_FALSE(R.NewType.has_value());
ASSERT_TRUE(R.Skip.has_value());
- EXPECT_EQ(*R.Skip, ReportReason::MultiLevelPointer);
+ ASSERT_TRUE(*R.Skip == ReportReason::MultiLevelPointer);
}
TEST_F(CppBoundedBuffersTest, MessageForIsNonEmpty) {
- for (ReportReason Reason :
- {ReportReason::MultiLevelPointer, ReportReason::PointerToArray,
- ReportReason::ReferenceToPointer, ReportReason::MultiDimensionalArray,
- ReportReason::IncompleteArray, ReportReason::UnreproducibleType,
- ReportReason::DeclarationGroup, ReportReason::MacroExpansion,
- ReportReason::TrailingReturnType, ReportReason::EmissionFailed,
- ReportReason::NotTransformed})
+ // Walks the whole enum rather than a hand-kept list, so a reason added
+ // without a message is caught here and not silently left untested.
+ for (unsigned I = 0; I <= static_cast<unsigned>(ReportReason::UnnamableType);
+ ++I) {
+ auto Reason = static_cast<ReportReason>(I);
EXPECT_FALSE(messageFor(Reason).empty());
+ }
+}
+
+TEST_F(CppBoundedBuffersTest, MessageForIsUnique) {
+ // Two reasons sharing a message would make reports ambiguous.
+ llvm::StringSet<> Seen;
+ for (unsigned I = 0; I <= static_cast<unsigned>(ReportReason::UnnamableType);
+ ++I) {
+ auto Reason = static_cast<ReportReason>(I);
+ EXPECT_TRUE(Seen.insert(messageFor(Reason)).second)
+ << "duplicate message: " << messageFor(Reason).str();
+ }
}
} // namespace
>From 45daeb11d9bb37413bc1f4df747425f6c85fe8d8 Mon Sep 17 00:00:00 2001
From: Ziqing Luo <ziqing_luo at apple.com>
Date: Sat, 15 Aug 2026 16:57:17 -0700
Subject: [PATCH 3/3] fix test
---
.../SourceTransformation/CppBoundedBuffersTest.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp b/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
index 2bc5dfda48852..a04070c50270a 100644
--- a/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
@@ -202,7 +202,7 @@ TEST_F(CppBoundedBuffersTest, PointerField) {
TEST_F(CppBoundedBuffersTest, ArrayOfPointers) {
Captured C = run("int *a[10];",
[](ASTContext &Ctx) { return varEntity("a", Ctx); }, {1});
- ASSERT_TRUE(C.Rewritten == "bounded_array<int *, 10> a;");
+ ASSERT_TRUE(C.Rewritten == "bounded_array<int *, 10>a;");
EXPECT_TRUE(C.Reports.empty());
}
More information about the cfe-commits
mailing list