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