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