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