BasicObjCFoundationChecks.cpp revision 9765ea9f755be50bb571100b44865f488e958d6d
1//== BasicObjCFoundationChecks.cpp - Simple Apple-Foundation checks -*- C++ -*-- 2// 3// The LLVM Compiler Infrastructure 4// 5// This file is distributed under the University of Illinois Open Source 6// License. See LICENSE.TXT for details. 7// 8//===----------------------------------------------------------------------===// 9// 10// This file defines BasicObjCFoundationChecks, a class that encapsulates 11// a set of simple checks to run on Objective-C code using Apple's Foundation 12// classes. 13// 14//===----------------------------------------------------------------------===// 15 16#include "ClangSACheckers.h" 17#include "clang/Analysis/DomainSpecific/CocoaConventions.h" 18#include "clang/StaticAnalyzer/Core/Checker.h" 19#include "clang/StaticAnalyzer/Core/CheckerManager.h" 20#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" 21#include "clang/StaticAnalyzer/Core/PathSensitive/ExplodedGraph.h" 22#include "clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h" 23#include "clang/StaticAnalyzer/Core/PathSensitive/ObjCMessage.h" 24#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h" 25#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" 26#include "clang/StaticAnalyzer/Core/PathSensitive/MemRegion.h" 27#include "clang/AST/DeclObjC.h" 28#include "clang/AST/Expr.h" 29#include "clang/AST/ExprObjC.h" 30#include "clang/AST/ASTContext.h" 31#include "llvm/ADT/SmallString.h" 32#include "llvm/ADT/StringMap.h" 33 34using namespace clang; 35using namespace ento; 36 37namespace { 38class APIMisuse : public BugType { 39public: 40 APIMisuse(const char* name) : BugType(name, "API Misuse (Apple)") {} 41}; 42} // end anonymous namespace 43 44//===----------------------------------------------------------------------===// 45// Utility functions. 46//===----------------------------------------------------------------------===// 47 48static const char* GetReceiverNameType(const ObjCMessage &msg) { 49 if (const ObjCInterfaceDecl *ID = msg.getReceiverInterface()) 50 return ID->getIdentifier()->getNameStart(); 51 return 0; 52} 53 54enum FoundationClass { 55 FC_None, 56 FC_NSArray, 57 FC_NSDictionary, 58 FC_NSEnumerator, 59 FC_NSOrderedSet, 60 FC_NSSet, 61 FC_NSString 62}; 63 64static FoundationClass findKnownClass(const ObjCInterfaceDecl *ID) { 65 static llvm::StringMap<FoundationClass> Classes; 66 if (Classes.empty()) { 67 Classes["NSArray"] = FC_NSArray; 68 Classes["NSDictionary"] = FC_NSDictionary; 69 Classes["NSEnumerator"] = FC_NSEnumerator; 70 Classes["NSOrderedSet"] = FC_NSOrderedSet; 71 Classes["NSSet"] = FC_NSSet; 72 Classes["NSString"] = FC_NSString; 73 } 74 75 // FIXME: Should we cache this at all? 76 FoundationClass result = Classes.lookup(ID->getIdentifier()->getName()); 77 if (result == FC_None) 78 if (const ObjCInterfaceDecl *Super = ID->getSuperClass()) 79 return findKnownClass(Super); 80 81 return result; 82} 83 84static inline bool isNil(SVal X) { 85 return isa<loc::ConcreteInt>(X); 86} 87 88//===----------------------------------------------------------------------===// 89// NilArgChecker - Check for prohibited nil arguments to ObjC method calls. 90//===----------------------------------------------------------------------===// 91 92namespace { 93 class NilArgChecker : public Checker<check::PreObjCMessage> { 94 mutable OwningPtr<APIMisuse> BT; 95 96 void WarnNilArg(CheckerContext &C, 97 const ObjCMessage &msg, unsigned Arg) const; 98 99 public: 100 void checkPreObjCMessage(ObjCMessage msg, CheckerContext &C) const; 101 }; 102} 103 104void NilArgChecker::WarnNilArg(CheckerContext &C, 105 const ObjCMessage &msg, 106 unsigned int Arg) const 107{ 108 if (!BT) 109 BT.reset(new APIMisuse("nil argument")); 110 111 if (ExplodedNode *N = C.generateSink()) { 112 SmallString<128> sbuf; 113 llvm::raw_svector_ostream os(sbuf); 114 os << "Argument to '" << GetReceiverNameType(msg) << "' method '" 115 << msg.getSelector().getAsString() << "' cannot be nil"; 116 117 BugReport *R = new BugReport(*BT, os.str(), N); 118 R->addRange(msg.getArgSourceRange(Arg)); 119 C.EmitReport(R); 120 } 121} 122 123void NilArgChecker::checkPreObjCMessage(ObjCMessage msg, 124 CheckerContext &C) const { 125 const ObjCInterfaceDecl *ID = msg.getReceiverInterface(); 126 if (!ID) 127 return; 128 129 if (findKnownClass(ID) == FC_NSString) { 130 Selector S = msg.getSelector(); 131 132 if (S.isUnarySelector()) 133 return; 134 135 // FIXME: This is going to be really slow doing these checks with 136 // lexical comparisons. 137 138 std::string NameStr = S.getAsString(); 139 StringRef Name(NameStr); 140 assert(!Name.empty()); 141 142 // FIXME: Checking for initWithFormat: will not work in most cases 143 // yet because [NSString alloc] returns id, not NSString*. We will 144 // need support for tracking expected-type information in the analyzer 145 // to find these errors. 146 if (Name == "caseInsensitiveCompare:" || 147 Name == "compare:" || 148 Name == "compare:options:" || 149 Name == "compare:options:range:" || 150 Name == "compare:options:range:locale:" || 151 Name == "componentsSeparatedByCharactersInSet:" || 152 Name == "initWithFormat:") { 153 if (isNil(msg.getArgSVal(0, C.getLocationContext(), C.getState()))) 154 WarnNilArg(C, msg, 0); 155 } 156 } 157} 158 159//===----------------------------------------------------------------------===// 160// Error reporting. 161//===----------------------------------------------------------------------===// 162 163namespace { 164class CFNumberCreateChecker : public Checker< check::PreStmt<CallExpr> > { 165 mutable OwningPtr<APIMisuse> BT; 166 mutable IdentifierInfo* II; 167public: 168 CFNumberCreateChecker() : II(0) {} 169 170 void checkPreStmt(const CallExpr *CE, CheckerContext &C) const; 171 172private: 173 void EmitError(const TypedRegion* R, const Expr *Ex, 174 uint64_t SourceSize, uint64_t TargetSize, uint64_t NumberKind); 175}; 176} // end anonymous namespace 177 178enum CFNumberType { 179 kCFNumberSInt8Type = 1, 180 kCFNumberSInt16Type = 2, 181 kCFNumberSInt32Type = 3, 182 kCFNumberSInt64Type = 4, 183 kCFNumberFloat32Type = 5, 184 kCFNumberFloat64Type = 6, 185 kCFNumberCharType = 7, 186 kCFNumberShortType = 8, 187 kCFNumberIntType = 9, 188 kCFNumberLongType = 10, 189 kCFNumberLongLongType = 11, 190 kCFNumberFloatType = 12, 191 kCFNumberDoubleType = 13, 192 kCFNumberCFIndexType = 14, 193 kCFNumberNSIntegerType = 15, 194 kCFNumberCGFloatType = 16 195}; 196 197namespace { 198 template<typename T> 199 class Optional { 200 bool IsKnown; 201 T Val; 202 public: 203 Optional() : IsKnown(false), Val(0) {} 204 Optional(const T& val) : IsKnown(true), Val(val) {} 205 206 bool isKnown() const { return IsKnown; } 207 208 const T& getValue() const { 209 assert (isKnown()); 210 return Val; 211 } 212 213 operator const T&() const { 214 return getValue(); 215 } 216 }; 217} 218 219static Optional<uint64_t> GetCFNumberSize(ASTContext &Ctx, uint64_t i) { 220 static const unsigned char FixedSize[] = { 8, 16, 32, 64, 32, 64 }; 221 222 if (i < kCFNumberCharType) 223 return FixedSize[i-1]; 224 225 QualType T; 226 227 switch (i) { 228 case kCFNumberCharType: T = Ctx.CharTy; break; 229 case kCFNumberShortType: T = Ctx.ShortTy; break; 230 case kCFNumberIntType: T = Ctx.IntTy; break; 231 case kCFNumberLongType: T = Ctx.LongTy; break; 232 case kCFNumberLongLongType: T = Ctx.LongLongTy; break; 233 case kCFNumberFloatType: T = Ctx.FloatTy; break; 234 case kCFNumberDoubleType: T = Ctx.DoubleTy; break; 235 case kCFNumberCFIndexType: 236 case kCFNumberNSIntegerType: 237 case kCFNumberCGFloatType: 238 // FIXME: We need a way to map from names to Type*. 239 default: 240 return Optional<uint64_t>(); 241 } 242 243 return Ctx.getTypeSize(T); 244} 245 246#if 0 247static const char* GetCFNumberTypeStr(uint64_t i) { 248 static const char* Names[] = { 249 "kCFNumberSInt8Type", 250 "kCFNumberSInt16Type", 251 "kCFNumberSInt32Type", 252 "kCFNumberSInt64Type", 253 "kCFNumberFloat32Type", 254 "kCFNumberFloat64Type", 255 "kCFNumberCharType", 256 "kCFNumberShortType", 257 "kCFNumberIntType", 258 "kCFNumberLongType", 259 "kCFNumberLongLongType", 260 "kCFNumberFloatType", 261 "kCFNumberDoubleType", 262 "kCFNumberCFIndexType", 263 "kCFNumberNSIntegerType", 264 "kCFNumberCGFloatType" 265 }; 266 267 return i <= kCFNumberCGFloatType ? Names[i-1] : "Invalid CFNumberType"; 268} 269#endif 270 271void CFNumberCreateChecker::checkPreStmt(const CallExpr *CE, 272 CheckerContext &C) const { 273 ProgramStateRef state = C.getState(); 274 const FunctionDecl *FD = C.getCalleeDecl(CE); 275 if (!FD) 276 return; 277 278 ASTContext &Ctx = C.getASTContext(); 279 if (!II) 280 II = &Ctx.Idents.get("CFNumberCreate"); 281 282 if (FD->getIdentifier() != II || CE->getNumArgs() != 3) 283 return; 284 285 // Get the value of the "theType" argument. 286 const LocationContext *LCtx = C.getLocationContext(); 287 SVal TheTypeVal = state->getSVal(CE->getArg(1), LCtx); 288 289 // FIXME: We really should allow ranges of valid theType values, and 290 // bifurcate the state appropriately. 291 nonloc::ConcreteInt* V = dyn_cast<nonloc::ConcreteInt>(&TheTypeVal); 292 if (!V) 293 return; 294 295 uint64_t NumberKind = V->getValue().getLimitedValue(); 296 Optional<uint64_t> TargetSize = GetCFNumberSize(Ctx, NumberKind); 297 298 // FIXME: In some cases we can emit an error. 299 if (!TargetSize.isKnown()) 300 return; 301 302 // Look at the value of the integer being passed by reference. Essentially 303 // we want to catch cases where the value passed in is not equal to the 304 // size of the type being created. 305 SVal TheValueExpr = state->getSVal(CE->getArg(2), LCtx); 306 307 // FIXME: Eventually we should handle arbitrary locations. We can do this 308 // by having an enhanced memory model that does low-level typing. 309 loc::MemRegionVal* LV = dyn_cast<loc::MemRegionVal>(&TheValueExpr); 310 if (!LV) 311 return; 312 313 const TypedValueRegion* R = dyn_cast<TypedValueRegion>(LV->stripCasts()); 314 if (!R) 315 return; 316 317 QualType T = Ctx.getCanonicalType(R->getValueType()); 318 319 // FIXME: If the pointee isn't an integer type, should we flag a warning? 320 // People can do weird stuff with pointers. 321 322 if (!T->isIntegerType()) 323 return; 324 325 uint64_t SourceSize = Ctx.getTypeSize(T); 326 327 // CHECK: is SourceSize == TargetSize 328 if (SourceSize == TargetSize) 329 return; 330 331 // Generate an error. Only generate a sink if 'SourceSize < TargetSize'; 332 // otherwise generate a regular node. 333 // 334 // FIXME: We can actually create an abstract "CFNumber" object that has 335 // the bits initialized to the provided values. 336 // 337 if (ExplodedNode *N = SourceSize < TargetSize ? C.generateSink() 338 : C.addTransition()) { 339 SmallString<128> sbuf; 340 llvm::raw_svector_ostream os(sbuf); 341 342 os << (SourceSize == 8 ? "An " : "A ") 343 << SourceSize << " bit integer is used to initialize a CFNumber " 344 "object that represents " 345 << (TargetSize == 8 ? "an " : "a ") 346 << TargetSize << " bit integer. "; 347 348 if (SourceSize < TargetSize) 349 os << (TargetSize - SourceSize) 350 << " bits of the CFNumber value will be garbage." ; 351 else 352 os << (SourceSize - TargetSize) 353 << " bits of the input integer will be lost."; 354 355 if (!BT) 356 BT.reset(new APIMisuse("Bad use of CFNumberCreate")); 357 358 BugReport *report = new BugReport(*BT, os.str(), N); 359 report->addRange(CE->getArg(2)->getSourceRange()); 360 C.EmitReport(report); 361 } 362} 363 364//===----------------------------------------------------------------------===// 365// CFRetain/CFRelease checking for null arguments. 366//===----------------------------------------------------------------------===// 367 368namespace { 369class CFRetainReleaseChecker : public Checker< check::PreStmt<CallExpr> > { 370 mutable OwningPtr<APIMisuse> BT; 371 mutable IdentifierInfo *Retain, *Release; 372public: 373 CFRetainReleaseChecker(): Retain(0), Release(0) {} 374 void checkPreStmt(const CallExpr *CE, CheckerContext &C) const; 375}; 376} // end anonymous namespace 377 378 379void CFRetainReleaseChecker::checkPreStmt(const CallExpr *CE, 380 CheckerContext &C) const { 381 // If the CallExpr doesn't have exactly 1 argument just give up checking. 382 if (CE->getNumArgs() != 1) 383 return; 384 385 ProgramStateRef state = C.getState(); 386 const FunctionDecl *FD = C.getCalleeDecl(CE); 387 if (!FD) 388 return; 389 390 if (!BT) { 391 ASTContext &Ctx = C.getASTContext(); 392 Retain = &Ctx.Idents.get("CFRetain"); 393 Release = &Ctx.Idents.get("CFRelease"); 394 BT.reset(new APIMisuse("null passed to CFRetain/CFRelease")); 395 } 396 397 // Check if we called CFRetain/CFRelease. 398 const IdentifierInfo *FuncII = FD->getIdentifier(); 399 if (!(FuncII == Retain || FuncII == Release)) 400 return; 401 402 // FIXME: The rest of this just checks that the argument is non-null. 403 // It should probably be refactored and combined with AttrNonNullChecker. 404 405 // Get the argument's value. 406 const Expr *Arg = CE->getArg(0); 407 SVal ArgVal = state->getSVal(Arg, C.getLocationContext()); 408 DefinedSVal *DefArgVal = dyn_cast<DefinedSVal>(&ArgVal); 409 if (!DefArgVal) 410 return; 411 412 // Get a NULL value. 413 SValBuilder &svalBuilder = C.getSValBuilder(); 414 DefinedSVal zero = cast<DefinedSVal>(svalBuilder.makeZeroVal(Arg->getType())); 415 416 // Make an expression asserting that they're equal. 417 DefinedOrUnknownSVal ArgIsNull = svalBuilder.evalEQ(state, zero, *DefArgVal); 418 419 // Are they equal? 420 ProgramStateRef stateTrue, stateFalse; 421 llvm::tie(stateTrue, stateFalse) = state->assume(ArgIsNull); 422 423 if (stateTrue && !stateFalse) { 424 ExplodedNode *N = C.generateSink(stateTrue); 425 if (!N) 426 return; 427 428 const char *description = (FuncII == Retain) 429 ? "Null pointer argument in call to CFRetain" 430 : "Null pointer argument in call to CFRelease"; 431 432 BugReport *report = new BugReport(*BT, description, N); 433 report->addRange(Arg->getSourceRange()); 434 report->addVisitor(bugreporter::getTrackNullOrUndefValueVisitor(N, Arg, 435 report)); 436 C.EmitReport(report); 437 return; 438 } 439 440 // From here on, we know the argument is non-null. 441 C.addTransition(stateFalse); 442} 443 444//===----------------------------------------------------------------------===// 445// Check for sending 'retain', 'release', or 'autorelease' directly to a Class. 446//===----------------------------------------------------------------------===// 447 448namespace { 449class ClassReleaseChecker : public Checker<check::PreObjCMessage> { 450 mutable Selector releaseS; 451 mutable Selector retainS; 452 mutable Selector autoreleaseS; 453 mutable Selector drainS; 454 mutable OwningPtr<BugType> BT; 455 456public: 457 void checkPreObjCMessage(ObjCMessage msg, CheckerContext &C) const; 458}; 459} 460 461void ClassReleaseChecker::checkPreObjCMessage(ObjCMessage msg, 462 CheckerContext &C) const { 463 464 if (!BT) { 465 BT.reset(new APIMisuse("message incorrectly sent to class instead of class " 466 "instance")); 467 468 ASTContext &Ctx = C.getASTContext(); 469 releaseS = GetNullarySelector("release", Ctx); 470 retainS = GetNullarySelector("retain", Ctx); 471 autoreleaseS = GetNullarySelector("autorelease", Ctx); 472 drainS = GetNullarySelector("drain", Ctx); 473 } 474 475 if (msg.isInstanceMessage()) 476 return; 477 const ObjCInterfaceDecl *Class = msg.getReceiverInterface(); 478 assert(Class); 479 480 Selector S = msg.getSelector(); 481 if (!(S == releaseS || S == retainS || S == autoreleaseS || S == drainS)) 482 return; 483 484 if (ExplodedNode *N = C.addTransition()) { 485 SmallString<200> buf; 486 llvm::raw_svector_ostream os(buf); 487 488 os << "The '" << S.getAsString() << "' message should be sent to instances " 489 "of class '" << Class->getName() 490 << "' and not the class directly"; 491 492 BugReport *report = new BugReport(*BT, os.str(), N); 493 report->addRange(msg.getSourceRange()); 494 C.EmitReport(report); 495 } 496} 497 498//===----------------------------------------------------------------------===// 499// Check for passing non-Objective-C types to variadic methods that expect 500// only Objective-C types. 501//===----------------------------------------------------------------------===// 502 503namespace { 504class VariadicMethodTypeChecker : public Checker<check::PreObjCMessage> { 505 mutable Selector arrayWithObjectsS; 506 mutable Selector dictionaryWithObjectsAndKeysS; 507 mutable Selector setWithObjectsS; 508 mutable Selector orderedSetWithObjectsS; 509 mutable Selector initWithObjectsS; 510 mutable Selector initWithObjectsAndKeysS; 511 mutable OwningPtr<BugType> BT; 512 513 bool isVariadicMessage(const ObjCMessage &msg) const; 514 515public: 516 void checkPreObjCMessage(ObjCMessage msg, CheckerContext &C) const; 517}; 518} 519 520/// isVariadicMessage - Returns whether the given message is a variadic message, 521/// where all arguments must be Objective-C types. 522bool 523VariadicMethodTypeChecker::isVariadicMessage(const ObjCMessage &msg) const { 524 const ObjCMethodDecl *MD = msg.getMethodDecl(); 525 526 if (!MD || !MD->isVariadic() || isa<ObjCProtocolDecl>(MD->getDeclContext())) 527 return false; 528 529 Selector S = msg.getSelector(); 530 531 if (msg.isInstanceMessage()) { 532 // FIXME: Ideally we'd look at the receiver interface here, but that's not 533 // useful for init, because alloc returns 'id'. In theory, this could lead 534 // to false positives, for example if there existed a class that had an 535 // initWithObjects: implementation that does accept non-Objective-C pointer 536 // types, but the chance of that happening is pretty small compared to the 537 // gains that this analysis gives. 538 const ObjCInterfaceDecl *Class = MD->getClassInterface(); 539 540 switch (findKnownClass(Class)) { 541 case FC_NSArray: 542 case FC_NSOrderedSet: 543 case FC_NSSet: 544 return S == initWithObjectsS; 545 case FC_NSDictionary: 546 return S == initWithObjectsAndKeysS; 547 default: 548 return false; 549 } 550 } else { 551 const ObjCInterfaceDecl *Class = msg.getReceiverInterface(); 552 553 switch (findKnownClass(Class)) { 554 case FC_NSArray: 555 return S == arrayWithObjectsS; 556 case FC_NSOrderedSet: 557 return S == orderedSetWithObjectsS; 558 case FC_NSSet: 559 return S == setWithObjectsS; 560 case FC_NSDictionary: 561 return S == dictionaryWithObjectsAndKeysS; 562 default: 563 return false; 564 } 565 } 566} 567 568void VariadicMethodTypeChecker::checkPreObjCMessage(ObjCMessage msg, 569 CheckerContext &C) const { 570 if (!BT) { 571 BT.reset(new APIMisuse("Arguments passed to variadic method aren't all " 572 "Objective-C pointer types")); 573 574 ASTContext &Ctx = C.getASTContext(); 575 arrayWithObjectsS = GetUnarySelector("arrayWithObjects", Ctx); 576 dictionaryWithObjectsAndKeysS = 577 GetUnarySelector("dictionaryWithObjectsAndKeys", Ctx); 578 setWithObjectsS = GetUnarySelector("setWithObjects", Ctx); 579 orderedSetWithObjectsS = GetUnarySelector("orderedSetWithObjects", Ctx); 580 581 initWithObjectsS = GetUnarySelector("initWithObjects", Ctx); 582 initWithObjectsAndKeysS = GetUnarySelector("initWithObjectsAndKeys", Ctx); 583 } 584 585 if (!isVariadicMessage(msg)) 586 return; 587 588 // We are not interested in the selector arguments since they have 589 // well-defined types, so the compiler will issue a warning for them. 590 unsigned variadicArgsBegin = msg.getSelector().getNumArgs(); 591 592 // We're not interested in the last argument since it has to be nil or the 593 // compiler would have issued a warning for it elsewhere. 594 unsigned variadicArgsEnd = msg.getNumArgs() - 1; 595 596 if (variadicArgsEnd <= variadicArgsBegin) 597 return; 598 599 // Verify that all arguments have Objective-C types. 600 llvm::Optional<ExplodedNode*> errorNode; 601 ProgramStateRef state = C.getState(); 602 603 for (unsigned I = variadicArgsBegin; I != variadicArgsEnd; ++I) { 604 QualType ArgTy = msg.getArgType(I); 605 if (ArgTy->isObjCObjectPointerType()) 606 continue; 607 608 // Block pointers are treaded as Objective-C pointers. 609 if (ArgTy->isBlockPointerType()) 610 continue; 611 612 // Ignore pointer constants. 613 if (isa<loc::ConcreteInt>(msg.getArgSVal(I, C.getLocationContext(), 614 state))) 615 continue; 616 617 // Ignore pointer types annotated with 'NSObject' attribute. 618 if (C.getASTContext().isObjCNSObjectType(ArgTy)) 619 continue; 620 621 // Ignore CF references, which can be toll-free bridged. 622 if (coreFoundation::isCFObjectRef(ArgTy)) 623 continue; 624 625 // Generate only one error node to use for all bug reports. 626 if (!errorNode.hasValue()) { 627 errorNode = C.addTransition(); 628 } 629 630 if (!errorNode.getValue()) 631 continue; 632 633 SmallString<128> sbuf; 634 llvm::raw_svector_ostream os(sbuf); 635 636 if (const char *TypeName = GetReceiverNameType(msg)) 637 os << "Argument to '" << TypeName << "' method '"; 638 else 639 os << "Argument to method '"; 640 641 os << msg.getSelector().getAsString() 642 << "' should be an Objective-C pointer type, not '" 643 << ArgTy.getAsString() << "'"; 644 645 BugReport *R = new BugReport(*BT, os.str(), 646 errorNode.getValue()); 647 R->addRange(msg.getArgSourceRange(I)); 648 C.EmitReport(R); 649 } 650} 651 652//===----------------------------------------------------------------------===// 653// Check registration. 654//===----------------------------------------------------------------------===// 655 656void ento::registerNilArgChecker(CheckerManager &mgr) { 657 mgr.registerChecker<NilArgChecker>(); 658} 659 660void ento::registerCFNumberCreateChecker(CheckerManager &mgr) { 661 mgr.registerChecker<CFNumberCreateChecker>(); 662} 663 664void ento::registerCFRetainReleaseChecker(CheckerManager &mgr) { 665 mgr.registerChecker<CFRetainReleaseChecker>(); 666} 667 668void ento::registerClassReleaseChecker(CheckerManager &mgr) { 669 mgr.registerChecker<ClassReleaseChecker>(); 670} 671 672void ento::registerVariadicMethodTypeChecker(CheckerManager &mgr) { 673 mgr.registerChecker<VariadicMethodTypeChecker>(); 674} 675