Skip to content

Commit aeeb541

Browse files
committed
[memory analysis] extend GetAllocType to see through optional-like wrapper constructions
Restrict the pointer-arithmetic Unknown-downgrade in VisitBinaryOperator to pointer-typed variables only, so unrelated scalar compound-assignments no longer poison tracked results. Allow AnalyzeAllocType to also traverse record-typed (class/struct) return values, not just raw pointers, so wrapper types like std::optional<T*> get analyzed instead of short-circuiting to None. In handleExpr: - Unwrap unary operator* calls (e.g. on an optional) to recurse into the wrapped value instead of falling back to a call analysis. - Detect nullptr/NULL literals directly via CXXNullPtrLiteralExpr/ GNUNullExpr, since the existing CK_NullToPointer cast walk cannot see through a MaterializeTemporaryExpr (e.g. nullptr_t passed to optional's converting constructor). - Recognize implicit converting-constructor calls (ExprWithCleanups -> ImplicitCastExpr(CK_ConstructorConversion) -> CXXConstructExpr) and recurse into the constructor's argument, so New/Malloc/etc. tags propagate through implicit wrapping (e.g. ). - Add isNullOpt() to recognize std::nullopt/cpp::nullopt construction as AllocType::Null. Known limitation (documented via FIXME + test): optional's copy/move constructor is not a converting constructor, so copying an optional loses its tracked AllocType and reports None. Add FunctionReflection_GetAllocType coverage (func60-func69) for: implicit optional-wrapping of new, std::nullopt join behavior, default-constructed empty structs, direct/list-initialized plain wrapper structs, operator* unwrap (unary vs. binary), direct nullptr returns through optional, and pointer arithmetic inside a for-loop increment clause.
1 parent ff9874a commit aeeb541

2 files changed

Lines changed: 128 additions & 5 deletions

File tree

lib/CppInterOp/CppInterOp.cpp

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1798,7 +1798,7 @@ struct AllocationTraverser : RecursiveASTVisitor<AllocationTraverser> {
17981798
varMap[VD] = handleExpr(RHS);
17991799
return true;
18001800
}
1801-
if (BO->isCompoundAssignmentOp()) {
1801+
if (VD->getType()->isPointerType() && BO->isCompoundAssignmentOp()) {
18021802
if (undoLog)
18031803
undoLog->try_emplace(VD, varMap[VD]);
18041804
varMap[VD] = AllocType::Unknown;
@@ -1870,6 +1870,20 @@ struct AllocationTraverser : RecursiveASTVisitor<AllocationTraverser> {
18701870
return AllocType::New;
18711871
}
18721872

1873+
static bool isNullOpt(const clang::CXXConstructExpr* CCE) {
1874+
// Expected: std/cpp::optional constructor
1875+
const clang::CXXConstructorDecl* CCD = CCE->getConstructor();
1876+
// optional(nullopt_t) noexcept; -> one arg
1877+
if (CCD->getNumParams() != 1)
1878+
return false;
1879+
clang::QualType ParamTy =
1880+
CCD->getParamDecl(0)->getType().getNonReferenceType();
1881+
// FIXME: Copy constructor of optional is not handled
1882+
if (const auto* CRD = ParamTy->getAsCXXRecordDecl())
1883+
return CRD->getName() == "nullopt_t";
1884+
return false;
1885+
}
1886+
18731887
std::optional<AllocType> handleExpr(const clang::Expr* expr) {
18741888
const clang::Expr* finExpr = expr->IgnoreParenCasts();
18751889
// Case: return new __type__
@@ -1888,17 +1902,44 @@ struct AllocationTraverser : RecursiveASTVisitor<AllocationTraverser> {
18881902
}
18891903

18901904
// Case: malloc or another func call
1891-
if (const auto* CE = dyn_cast<CallExpr>(finExpr))
1905+
if (const auto* CE = dyn_cast<CallExpr>(finExpr)) {
1906+
// Case: std::optional<int*> ptr = new int; return *ptr;
1907+
if (const auto* COCE = dyn_cast<clang::CXXOperatorCallExpr>(CE)) {
1908+
if (COCE->getOperator() == OverloadedOperatorKind::OO_Star &&
1909+
!COCE->isInfixBinaryOp())
1910+
return handleExpr(COCE->getArg(0));
1911+
}
18921912
return handleCall(CE);
1913+
}
18931914

18941915
// Case: NULL, nullptr or (int*)(__integerLiteral__)
1916+
if (llvm::isa<clang::CXXNullPtrLiteralExpr>(finExpr) ||
1917+
llvm::isa<clang::GNUNullExpr>(finExpr))
1918+
return AllocType::Null;
18951919
const clang::Expr* parExpr = expr->IgnoreParens();
18961920
while (const auto* CE = dyn_cast<CastExpr>(parExpr)) {
18971921
if (CE->getCastKind() == CK_NullToPointer)
18981922
return AllocType::Null;
18991923
parExpr = CE->getSubExpr();
19001924
}
19011925

1926+
// Case: constructor cast
1927+
if (auto* CCE = dyn_cast<clang::CXXConstructExpr>(finExpr)) {
1928+
// std::nullopt or cpp::nullopt
1929+
if (isNullOpt(CCE))
1930+
return AllocType::Null;
1931+
const clang::Expr* stripped = expr->IgnoreParens();
1932+
// ExprWithCleanups -> ImplicitCastExpr -> CXXConstructExpr pattern
1933+
if (const auto* EWC = dyn_cast<clang::ExprWithCleanups>(stripped))
1934+
stripped = EWC->getSubExpr();
1935+
// We do not want to analyze explicit conversions
1936+
auto* ICE = dyn_cast<clang::ImplicitCastExpr>(stripped);
1937+
bool isImplicitConv =
1938+
ICE && ICE->getCastKind() == CK_ConstructorConversion;
1939+
// ImplicitCastExpr -> CXXConstructExpr pattern
1940+
if (isImplicitConv)
1941+
return handleExpr(CCE->getArg(0));
1942+
}
19021943
return AllocType::None;
19031944
}
19041945
};
@@ -1908,8 +1949,8 @@ static std::optional<AllocType>
19081949
AnalyzeAllocType(const clang::FunctionDecl* Fn,
19091950
std::unordered_map<const clang::FunctionDecl*,
19101951
std::optional<AllocType>>& visitedFuncs) {
1911-
const clang::QualType QT = Fn->getReturnType();
1912-
if (!QT->isPointerType())
1952+
QualType QT = Fn->getReturnType();
1953+
if (!QT->isPointerType() && !QT->isRecordType())
19131954
return AllocType::None;
19141955
const Stmt* fnBody = Fn->getBody();
19151956
if (!fnBody)

unittests/CppInterOp/FunctionReflectionTest.cpp

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) {
833833
std::string code = R"(
834834
#include <new>
835835
#include <stdlib.h>
836+
#include <optional>
836837
837838
int* func0(int n){ return new int(n); }
838839
@@ -1206,8 +1207,79 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) {
12061207
void* func58(){ return __builtin_operator_new(64); }
12071208
void* func59(){int* m = (int*)0; return malloc(sizeof(int));}
12081209
1210+
template <typename T>
1211+
std::optional<T*> func60_helper(){
1212+
T* ptr = new T;
1213+
return ptr;
1214+
}
1215+
1216+
std::optional<int*> func60(){
1217+
return func60_helper<int>();
1218+
}
1219+
1220+
std::optional<int*> func61(int n){
1221+
if(n>0)
1222+
return std::nullopt;
1223+
return new int;
1224+
}
1225+
1226+
struct Empty { Empty() {} };
1227+
Empty func62() {
1228+
Empty e;
1229+
return e;
1230+
}
1231+
1232+
struct Wrapper { int a, b; Wrapper(int x, int y) : a(x), b(y) {} };
1233+
Wrapper func63() {
1234+
int a = 1, b = 2;
1235+
return Wrapper(a, b);
1236+
}
1237+
1238+
Wrapper func64() {
1239+
int a = 1, b = 2;
1240+
return {a, b};
1241+
}
1242+
1243+
int* func65(){
1244+
auto ptr = func61(10);
1245+
if(ptr)
1246+
return *ptr;
1247+
return nullptr;
1248+
}
1249+
1250+
struct Box {
1251+
int* p;
1252+
Box(int* p) : p(p) {}
1253+
Box operator*(const Box& o) const { return Box(nullptr); }
1254+
};
1255+
Box func66() {
1256+
Box a(new int);
1257+
Box b(nullptr);
1258+
return a * b;
1259+
}
1260+
1261+
std::optional<int*> func67(){
1262+
return nullptr;
1263+
}
1264+
1265+
int* func68(){
1266+
int tmp = 10;
1267+
int* m = (int*)tmp;
1268+
for(int i = 0; i < 10; m+=1){
1269+
return m;
1270+
}
1271+
return m;
1272+
}
1273+
1274+
std::optional<int*> func69() {
1275+
std::optional<int*> a = new int;
1276+
//FIXME: Copy constructor is called, which takes one arg but is not nullopt constructor
1277+
//so it returns none, look at isNullOpt(const clang::CXXConstructExpr* CCE) function
1278+
std::optional<int*> b = a;
1279+
return b;
1280+
}
12091281
)";
1210-
TestFixture::CreateInterpreter();
1282+
TestFixture::CreateInterpreter({"-std=c++17"});
12111283
Interp->declare(code);
12121284

12131285
#define TESTAC(N, EXP) \
@@ -1275,6 +1347,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) {
12751347
TESTAC(57, None);
12761348
TESTAC(58, OperatorNew);
12771349
TESTAC(59, Malloc);
1350+
TESTAC(60, New);
1351+
TESTAC(61, New);
1352+
TESTAC(62, None);
1353+
TESTAC(63, None);
1354+
TESTAC(64, None);
1355+
TESTAC(65, New);
1356+
TESTAC(66, None);
1357+
TESTAC(67, Null);
1358+
TESTAC(68, Unknown);
1359+
TESTAC(69, None);
12781360
#undef TESTAC
12791361

12801362
Cpp::DeleteInterpreter();

0 commit comments

Comments
 (0)