-
Notifications
You must be signed in to change notification settings - Fork 13.4k
[NFC][analyzer] Multipart checker refactor 2: NullabilityChecker #132250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d4878a6
2708bed
3ca23ce
d490c74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,25 +112,30 @@ class NullabilityChecker | |
void printState(raw_ostream &Out, ProgramStateRef State, const char *NL, | ||
const char *Sep) const override; | ||
|
||
enum CheckKind { | ||
CK_NullPassedToNonnull, | ||
CK_NullReturnedFromNonnull, | ||
CK_NullableDereferenced, | ||
CK_NullablePassedToNonnull, | ||
CK_NullableReturnedFromNonnull, | ||
CK_NumCheckKinds | ||
// FIXME: This enumeration of checker parts is extremely similar to the | ||
// ErrorKind enum. It would be nice to unify them to simplify the code. | ||
enum : CheckerPartIdx { | ||
NullPassedToNonnullChecker, | ||
NullReturnedFromNonnullChecker, | ||
NullableDereferencedChecker, | ||
NullablePassedToNonnullChecker, | ||
NullableReturnedFromNonnullChecker, | ||
NumCheckerParts | ||
}; | ||
|
||
bool ChecksEnabled[CK_NumCheckKinds] = {false}; | ||
CheckerNameRef CheckNames[CK_NumCheckKinds]; | ||
mutable std::unique_ptr<BugType> BTs[CK_NumCheckKinds]; | ||
|
||
const std::unique_ptr<BugType> &getBugType(CheckKind Kind) const { | ||
if (!BTs[Kind]) | ||
BTs[Kind].reset(new BugType(CheckNames[Kind], "Nullability", | ||
categories::MemoryError)); | ||
return BTs[Kind]; | ||
} | ||
// FIXME: Currently the `Description` fields of these `BugType`s are all | ||
// identical ("Nullability") -- they should be more descriptive than this. | ||
BugType BugTypes[NumCheckerParts] = { | ||
{this, NullPassedToNonnullChecker, "Nullability", | ||
categories::MemoryError}, | ||
{this, NullReturnedFromNonnullChecker, "Nullability", | ||
categories::MemoryError}, | ||
{this, NullableDereferencedChecker, "Nullability", | ||
categories::MemoryError}, | ||
{this, NullablePassedToNonnullChecker, "Nullability", | ||
categories::MemoryError}, | ||
{this, NullableReturnedFromNonnullChecker, "Nullability", | ||
categories::MemoryError}}; | ||
|
||
// When set to false no nullability information will be tracked in | ||
// NullabilityMap. It is possible to catch errors like passing a null pointer | ||
|
@@ -163,17 +168,16 @@ class NullabilityChecker | |
/// | ||
/// When \p SuppressPath is set to true, no more bugs will be reported on this | ||
/// path by this checker. | ||
void reportBugIfInvariantHolds(StringRef Msg, ErrorKind Error, CheckKind CK, | ||
ExplodedNode *N, const MemRegion *Region, | ||
CheckerContext &C, | ||
void reportBugIfInvariantHolds(StringRef Msg, ErrorKind Error, | ||
const BugType &BT, ExplodedNode *N, | ||
const MemRegion *Region, CheckerContext &C, | ||
const Stmt *ValueExpr = nullptr, | ||
bool SuppressPath = false) const; | ||
|
||
void reportBug(StringRef Msg, ErrorKind Error, CheckKind CK, ExplodedNode *N, | ||
const MemRegion *Region, BugReporter &BR, | ||
void reportBug(StringRef Msg, ErrorKind Error, const BugType &BT, | ||
ExplodedNode *N, const MemRegion *Region, BugReporter &BR, | ||
const Stmt *ValueExpr = nullptr) const { | ||
const std::unique_ptr<BugType> &BT = getBugType(CK); | ||
auto R = std::make_unique<PathSensitiveBugReport>(*BT, Msg, N); | ||
auto R = std::make_unique<PathSensitiveBugReport>(BT, Msg, N); | ||
if (Region) { | ||
R->markInteresting(Region); | ||
R->addVisitor<NullabilityBugVisitor>(Region); | ||
|
@@ -479,7 +483,7 @@ static bool checkInvariantViolation(ProgramStateRef State, ExplodedNode *N, | |
} | ||
|
||
void NullabilityChecker::reportBugIfInvariantHolds( | ||
StringRef Msg, ErrorKind Error, CheckKind CK, ExplodedNode *N, | ||
StringRef Msg, ErrorKind Error, const BugType &BT, ExplodedNode *N, | ||
const MemRegion *Region, CheckerContext &C, const Stmt *ValueExpr, | ||
bool SuppressPath) const { | ||
ProgramStateRef OriginalState = N->getState(); | ||
|
@@ -491,7 +495,7 @@ void NullabilityChecker::reportBugIfInvariantHolds( | |
N = C.addTransition(OriginalState, N); | ||
} | ||
|
||
reportBug(Msg, Error, CK, N, Region, C.getBugReporter(), ValueExpr); | ||
reportBug(Msg, Error, BT, N, Region, C.getBugReporter(), ValueExpr); | ||
} | ||
|
||
/// Cleaning up the program state. | ||
|
@@ -545,20 +549,21 @@ void NullabilityChecker::checkEvent(ImplicitNullDerefEvent Event) const { | |
if (!TrackedNullability) | ||
return; | ||
|
||
if (ChecksEnabled[CK_NullableDereferenced] && | ||
if (isPartEnabled(NullableDereferencedChecker) && | ||
TrackedNullability->getValue() == Nullability::Nullable) { | ||
BugReporter &BR = *Event.BR; | ||
// Do not suppress errors on defensive code paths, because dereferencing | ||
// a nullable pointer is always an error. | ||
if (Event.IsDirectDereference) | ||
reportBug("Nullable pointer is dereferenced", | ||
ErrorKind::NullableDereferenced, CK_NullableDereferenced, | ||
Event.SinkNode, Region, BR); | ||
reportBug( | ||
"Nullable pointer is dereferenced", ErrorKind::NullableDereferenced, | ||
BugTypes[NullableDereferencedChecker], Event.SinkNode, Region, BR); | ||
else { | ||
reportBug("Nullable pointer is passed to a callee that requires a " | ||
"non-null", | ||
ErrorKind::NullablePassedToNonnull, CK_NullableDereferenced, | ||
Event.SinkNode, Region, BR); | ||
ErrorKind::NullablePassedToNonnull, | ||
BugTypes[NullableDereferencedChecker], Event.SinkNode, Region, | ||
BR); | ||
} | ||
} | ||
} | ||
|
@@ -711,7 +716,8 @@ void NullabilityChecker::checkPreStmt(const ReturnStmt *S, | |
|
||
bool NullReturnedFromNonNull = (RequiredNullability == Nullability::Nonnull && | ||
Nullness == NullConstraint::IsNull); | ||
if (ChecksEnabled[CK_NullReturnedFromNonnull] && NullReturnedFromNonNull && | ||
if (isPartEnabled(NullReturnedFromNonnullChecker) && | ||
NullReturnedFromNonNull && | ||
RetExprTypeLevelNullability != Nullability::Nonnull && | ||
!InSuppressedMethodFamily) { | ||
static CheckerProgramPointTag Tag(this, "NullReturnedFromNonnull"); | ||
|
@@ -725,8 +731,8 @@ void NullabilityChecker::checkPreStmt(const ReturnStmt *S, | |
OS << " returned from a " << C.getDeclDescription(D) << | ||
" that is expected to return a non-null value"; | ||
reportBugIfInvariantHolds(OS.str(), ErrorKind::NilReturnedToNonnull, | ||
CK_NullReturnedFromNonnull, N, nullptr, C, | ||
RetExpr); | ||
BugTypes[NullReturnedFromNonnullChecker], N, | ||
nullptr, C, RetExpr); | ||
return; | ||
} | ||
|
||
|
@@ -746,7 +752,7 @@ void NullabilityChecker::checkPreStmt(const ReturnStmt *S, | |
State->get<NullabilityMap>(Region); | ||
if (TrackedNullability) { | ||
Nullability TrackedNullabValue = TrackedNullability->getValue(); | ||
if (ChecksEnabled[CK_NullableReturnedFromNonnull] && | ||
if (isPartEnabled(NullableReturnedFromNonnullChecker) && | ||
Nullness != NullConstraint::IsNotNull && | ||
TrackedNullabValue == Nullability::Nullable && | ||
RequiredNullability == Nullability::Nonnull) { | ||
|
@@ -759,7 +765,8 @@ void NullabilityChecker::checkPreStmt(const ReturnStmt *S, | |
" that is expected to return a non-null value"; | ||
|
||
reportBugIfInvariantHolds(OS.str(), ErrorKind::NullableReturnedToNonnull, | ||
CK_NullableReturnedFromNonnull, N, Region, C); | ||
BugTypes[NullableReturnedFromNonnullChecker], N, | ||
Region, C); | ||
} | ||
return; | ||
} | ||
|
@@ -810,7 +817,7 @@ void NullabilityChecker::checkPreCall(const CallEvent &Call, | |
|
||
unsigned ParamIdx = Param->getFunctionScopeIndex() + 1; | ||
|
||
if (ChecksEnabled[CK_NullPassedToNonnull] && | ||
if (isPartEnabled(NullPassedToNonnullChecker) && | ||
Nullness == NullConstraint::IsNull && | ||
ArgExprTypeLevelNullability != Nullability::Nonnull && | ||
RequiredNullability == Nullability::Nonnull && | ||
|
@@ -825,8 +832,8 @@ void NullabilityChecker::checkPreCall(const CallEvent &Call, | |
OS << " passed to a callee that requires a non-null " << ParamIdx | ||
<< llvm::getOrdinalSuffix(ParamIdx) << " parameter"; | ||
reportBugIfInvariantHolds(OS.str(), ErrorKind::NilPassedToNonnull, | ||
CK_NullPassedToNonnull, N, nullptr, C, ArgExpr, | ||
/*SuppressPath=*/false); | ||
BugTypes[NullPassedToNonnullChecker], N, | ||
nullptr, C, ArgExpr, /*SuppressPath=*/false); | ||
return; | ||
} | ||
|
||
|
@@ -842,7 +849,7 @@ void NullabilityChecker::checkPreCall(const CallEvent &Call, | |
TrackedNullability->getValue() != Nullability::Nullable) | ||
continue; | ||
|
||
if (ChecksEnabled[CK_NullablePassedToNonnull] && | ||
if (isPartEnabled(NullablePassedToNonnullChecker) && | ||
RequiredNullability == Nullability::Nonnull && | ||
isDiagnosableCall(Call)) { | ||
ExplodedNode *N = C.addTransition(State); | ||
|
@@ -851,17 +858,17 @@ void NullabilityChecker::checkPreCall(const CallEvent &Call, | |
OS << "Nullable pointer is passed to a callee that requires a non-null " | ||
<< ParamIdx << llvm::getOrdinalSuffix(ParamIdx) << " parameter"; | ||
reportBugIfInvariantHolds(OS.str(), ErrorKind::NullablePassedToNonnull, | ||
CK_NullablePassedToNonnull, N, Region, C, | ||
ArgExpr, /*SuppressPath=*/true); | ||
BugTypes[NullablePassedToNonnullChecker], N, | ||
Region, C, ArgExpr, /*SuppressPath=*/true); | ||
return; | ||
} | ||
if (ChecksEnabled[CK_NullableDereferenced] && | ||
if (isPartEnabled(NullableDereferencedChecker) && | ||
Param->getType()->isReferenceType()) { | ||
ExplodedNode *N = C.addTransition(State); | ||
reportBugIfInvariantHolds("Nullable pointer is dereferenced", | ||
ErrorKind::NullableDereferenced, | ||
CK_NullableDereferenced, N, Region, C, | ||
ArgExpr, /*SuppressPath=*/true); | ||
BugTypes[NullableDereferencedChecker], N, | ||
Region, C, ArgExpr, /*SuppressPath=*/true); | ||
return; | ||
} | ||
continue; | ||
|
@@ -1295,7 +1302,7 @@ void NullabilityChecker::checkBind(SVal L, SVal V, const Stmt *S, | |
|
||
bool NullAssignedToNonNull = (LocNullability == Nullability::Nonnull && | ||
RhsNullness == NullConstraint::IsNull); | ||
if (ChecksEnabled[CK_NullPassedToNonnull] && NullAssignedToNonNull && | ||
if (isPartEnabled(NullPassedToNonnullChecker) && NullAssignedToNonNull && | ||
ValNullability != Nullability::Nonnull && | ||
ValueExprTypeLevelNullability != Nullability::Nonnull && | ||
!isARCNilInitializedLocal(C, S)) { | ||
|
@@ -1314,7 +1321,8 @@ void NullabilityChecker::checkBind(SVal L, SVal V, const Stmt *S, | |
OS << (LocType->isObjCObjectPointerType() ? "nil" : "Null"); | ||
OS << " assigned to a pointer which is expected to have non-null value"; | ||
reportBugIfInvariantHolds(OS.str(), ErrorKind::NilAssignedToNonnull, | ||
CK_NullPassedToNonnull, N, nullptr, C, ValueStmt); | ||
BugTypes[NullPassedToNonnullChecker], N, nullptr, | ||
C, ValueStmt); | ||
return; | ||
} | ||
|
||
|
@@ -1340,14 +1348,15 @@ void NullabilityChecker::checkBind(SVal L, SVal V, const Stmt *S, | |
if (RhsNullness == NullConstraint::IsNotNull || | ||
TrackedNullability->getValue() != Nullability::Nullable) | ||
return; | ||
if (ChecksEnabled[CK_NullablePassedToNonnull] && | ||
if (isPartEnabled(NullablePassedToNonnullChecker) && | ||
LocNullability == Nullability::Nonnull) { | ||
static CheckerProgramPointTag Tag(this, "NullablePassedToNonnull"); | ||
ExplodedNode *N = C.addTransition(State, C.getPredecessor(), &Tag); | ||
reportBugIfInvariantHolds("Nullable pointer is assigned to a pointer " | ||
"which is expected to have non-null value", | ||
ErrorKind::NullableAssignedToNonnull, | ||
CK_NullablePassedToNonnull, N, ValueRegion, C); | ||
BugTypes[NullablePassedToNonnullChecker], N, | ||
ValueRegion, C); | ||
} | ||
return; | ||
} | ||
|
@@ -1394,35 +1403,34 @@ void NullabilityChecker::printState(raw_ostream &Out, ProgramStateRef State, | |
} | ||
} | ||
|
||
void ento::registerNullabilityBase(CheckerManager &mgr) { | ||
mgr.registerChecker<NullabilityChecker>(); | ||
} | ||
|
||
bool ento::shouldRegisterNullabilityBase(const CheckerManager &mgr) { | ||
return true; | ||
} | ||
|
||
#define REGISTER_CHECKER(name, trackingRequired) \ | ||
void ento::register##name##Checker(CheckerManager &mgr) { \ | ||
NullabilityChecker *checker = mgr.getChecker<NullabilityChecker>(); \ | ||
checker->ChecksEnabled[NullabilityChecker::CK_##name] = true; \ | ||
checker->CheckNames[NullabilityChecker::CK_##name] = \ | ||
mgr.getCurrentCheckerName(); \ | ||
checker->NeedTracking = checker->NeedTracking || trackingRequired; \ | ||
checker->NoDiagnoseCallsToSystemHeaders = \ | ||
checker->NoDiagnoseCallsToSystemHeaders || \ | ||
mgr.getAnalyzerOptions().getCheckerBooleanOption( \ | ||
checker, "NoDiagnoseCallsToSystemHeaders", true); \ | ||
// The checker group "nullability" consists of the checkers that are | ||
// implemented as the parts of the checker class `NullabilityChecker`. These | ||
// checkers share a checker option "nullability:NoDiagnoseCallsToSystemHeaders" | ||
// which semantically belongs to the whole group and not just one checker from | ||
// it. As this is a unique situation (I don't know about any other similar | ||
// group-level option) there is no mechanism to inject this group name from | ||
// e.g. Checkers.td, so I'm just hardcoding it here. (These are old stable | ||
// checkers, I don't think that their name will change.) | ||
Comment on lines
+1406
to
+1413
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before this PR, the registration functions of the (real) checker parts actually tested the presence of the checker option This was working because in the checker option handling code specifying an option on the group level (e.g. saying In commit d490c74 I'm turning this option into a "real" group-level option by passing the group name to By the way the name of this option is really ugly -- it should be |
||
#define CHECKER_GROUP_NAME "nullability" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use a static constexpr StringLiteral instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's probably a better choice. |
||
|
||
#define REGISTER_CHECKER(Part, TrackingRequired) \ | ||
void ento::register##Part##Checker(CheckerManager &Mgr) { \ | ||
auto *Checker = Mgr.registerChecker<NullabilityChecker, \ | ||
NullabilityChecker::Part##Checker>(); \ | ||
Checker->NeedTracking = Checker->NeedTracking || TrackingRequired; \ | ||
Checker->NoDiagnoseCallsToSystemHeaders = \ | ||
Checker->NoDiagnoseCallsToSystemHeaders || \ | ||
Mgr.getAnalyzerOptions().getCheckerBooleanOption( \ | ||
CHECKER_GROUP_NAME, "NoDiagnoseCallsToSystemHeaders", true); \ | ||
} \ | ||
\ | ||
bool ento::shouldRegister##name##Checker(const CheckerManager &mgr) { \ | ||
bool ento::shouldRegister##Part##Checker(const CheckerManager &) { \ | ||
return true; \ | ||
} | ||
|
||
// The checks are likely to be turned on by default and it is possible to do | ||
// them without tracking any nullability related information. As an optimization | ||
// no nullability information will be tracked when only these two checks are | ||
// enables. | ||
// These checker parts are likely to be turned on by default and they don't | ||
// need the tracking of nullability related information. As an optimization | ||
// nullability information won't be tracked when the rest are disabled. | ||
REGISTER_CHECKER(NullPassedToNonnull, false) | ||
REGISTER_CHECKER(NullReturnedFromNonnull, false) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if you could hoist stuff like this to make these fit in 1 line each.
This way later you could just directly refer to
NullPassedToNonnullBug
.Every time I see this CheckerPart enum, I question myself, why do we need this? And I can't convince myself that this is the right way.
Correct me if I'm wrong, but we don't really need a BugType array. We also don't really need a checker part enum either.
We could design a system that is less coupled. Something like this:
The address of the CheckerPart object identifies the "part". The "CheckerPart" could contain a mutable bool flag to know if it's enabled or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have something like this in mind:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, you're completely right, and I should have noticed this -- e.g.
DivZeroChecker
also declares the bug types as individual variables (and not arrays).When I designed the current framework I implicitly assumed that we will need a checker part identifier type which acts as an index/key for a (perhaps associative) array containing the names / registration status, because this was a common element in all the individual checkers plus the old abandoned
SubChecker
draft commit that was written by Kristóf Umann several years ago. (I spent lots of time on thinking about details within this box, but I didn't consider out-of-the box ideas.) Now that I think about your suggestion, I see that it could lead to a simpler alternative framework.The operations that a checker part type needs to support:
BugType
is called from the constructor of a checker and needs to take a checker part argument (so the checker part should be available at this point).BugType
stores this checker part as a data member (so all checkers should use the sameCheckerPart
type).CheckerManager::registerChecker
takes a checker part as an argument (in fact, currently as a template argument, but this may change) and binds the name injected fromCheckers.td
to it. (Binding the name implicitly marks the part as enabled, because enabled parts need to have names, while others don't have names.)getTagDescription()
currently iterates over the checker parts to find one that has a name. This is the only case where iteration over checker parts happen, so perhaps it should be replaced by an alternative implementation.These could be satisfied by a checker part type that looks like
I think this could be a significant simplification -- I'll flesh it out in a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't see your detailed implementation idea before writing my own. I think storing the$\to$
CheckerNameRef
as a data member of theCheckerPart
is a significant advantage because this way we won't need aCheckerPart
CheckerNameRef
map as a data member of theCheckerBase
, but I will probably borrow some ideas from your approach.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to flash out an implementation. I'm happy that it inspired you.
I'd suggest blocking this PR until you played with the new ideas first.
There shouldn't be a rush for migrating to the current framework I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the ideas!
Completely agree.