Anna Zaks | e5d74ca | 2015-08-14 20:22:22 +0000 | [diff] [blame] | 1 | //=- LocalizationChecker.cpp -------------------------------------*- 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 a set of checks for localizability including: |
| 11 | // 1) A checker that warns about uses of non-localized NSStrings passed to |
| 12 | // UI methods expecting localized strings |
| 13 | // 2) A syntactic checker that warns against the bad practice of |
| 14 | // not including a comment in NSLocalizedString macros. |
| 15 | // |
| 16 | //===----------------------------------------------------------------------===// |
| 17 | |
| 18 | #include "ClangSACheckers.h" |
| 19 | #include "SelectorExtras.h" |
| 20 | #include "clang/AST/Attr.h" |
| 21 | #include "clang/AST/Decl.h" |
| 22 | #include "clang/AST/DeclObjC.h" |
| 23 | #include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h" |
| 24 | #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" |
| 25 | #include "clang/StaticAnalyzer/Core/Checker.h" |
| 26 | #include "clang/StaticAnalyzer/Core/CheckerManager.h" |
| 27 | #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" |
| 28 | #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" |
| 29 | #include "clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h" |
| 30 | #include "clang/Lex/Lexer.h" |
| 31 | #include "clang/AST/RecursiveASTVisitor.h" |
| 32 | #include "clang/AST/StmtVisitor.h" |
| 33 | #include "llvm/Support/Unicode.h" |
| 34 | #include "llvm/ADT/StringSet.h" |
| 35 | |
| 36 | using namespace clang; |
| 37 | using namespace ento; |
| 38 | |
| 39 | namespace { |
| 40 | struct LocalizedState { |
| 41 | private: |
| 42 | enum Kind { NonLocalized, Localized } K; |
| 43 | LocalizedState(Kind InK) : K(InK) {} |
| 44 | |
| 45 | public: |
| 46 | bool isLocalized() const { return K == Localized; } |
| 47 | bool isNonLocalized() const { return K == NonLocalized; } |
| 48 | |
| 49 | static LocalizedState getLocalized() { return LocalizedState(Localized); } |
| 50 | static LocalizedState getNonLocalized() { |
| 51 | return LocalizedState(NonLocalized); |
| 52 | } |
| 53 | |
| 54 | // Overload the == operator |
| 55 | bool operator==(const LocalizedState &X) const { return K == X.K; } |
| 56 | |
| 57 | // LLVMs equivalent of a hash function |
| 58 | void Profile(llvm::FoldingSetNodeID &ID) const { ID.AddInteger(K); } |
| 59 | }; |
| 60 | |
| 61 | class NonLocalizedStringChecker |
| 62 | : public Checker<check::PostCall, check::PreObjCMessage, |
| 63 | check::PostObjCMessage, |
| 64 | check::PostStmt<ObjCStringLiteral>> { |
| 65 | |
| 66 | mutable std::unique_ptr<BugType> BT; |
| 67 | |
| 68 | // Methods that require a localized string |
| 69 | mutable llvm::StringMap<llvm::StringMap<uint8_t>> UIMethods; |
| 70 | // Methods that return a localized string |
| 71 | mutable llvm::SmallSet<std::pair<StringRef, StringRef>, 12> LSM; |
| 72 | // C Functions that return a localized string |
| 73 | mutable llvm::StringSet<> LSF; |
| 74 | |
| 75 | void initUIMethods(ASTContext &Ctx) const; |
| 76 | void initLocStringsMethods(ASTContext &Ctx) const; |
| 77 | |
| 78 | bool hasNonLocalizedState(SVal S, CheckerContext &C) const; |
| 79 | bool hasLocalizedState(SVal S, CheckerContext &C) const; |
| 80 | void setNonLocalizedState(SVal S, CheckerContext &C) const; |
| 81 | void setLocalizedState(SVal S, CheckerContext &C) const; |
| 82 | |
| 83 | bool isAnnotatedAsLocalized(const Decl *D) const; |
| 84 | void reportLocalizationError(SVal S, const ObjCMethodCall &M, |
| 85 | CheckerContext &C, int argumentNumber = 0) const; |
| 86 | |
| 87 | public: |
| 88 | NonLocalizedStringChecker(); |
| 89 | |
| 90 | // When this parameter is set to true, the checker assumes all |
| 91 | // methods that return NSStrings are unlocalized. Thus, more false |
| 92 | // positives will be reported. |
| 93 | DefaultBool IsAggressive; |
| 94 | |
| 95 | void checkPreObjCMessage(const ObjCMethodCall &msg, CheckerContext &C) const; |
| 96 | void checkPostObjCMessage(const ObjCMethodCall &msg, CheckerContext &C) const; |
| 97 | void checkPostStmt(const ObjCStringLiteral *SL, CheckerContext &C) const; |
| 98 | void checkPostCall(const CallEvent &Call, CheckerContext &C) const; |
| 99 | }; |
| 100 | |
| 101 | } // end anonymous namespace |
| 102 | |
| 103 | REGISTER_MAP_WITH_PROGRAMSTATE(LocalizedMemMap, const MemRegion *, |
| 104 | LocalizedState) |
| 105 | |
| 106 | NonLocalizedStringChecker::NonLocalizedStringChecker() { |
| 107 | BT.reset(new BugType(this, "Unlocalized string", "Localizability Error")); |
| 108 | } |
| 109 | |
| 110 | /// Initializes a list of methods that require a localized string |
| 111 | /// Format: {"ClassName", {{"selectorName:", LocStringArg#}, ...}, ...} |
| 112 | void NonLocalizedStringChecker::initUIMethods(ASTContext &Ctx) const { |
| 113 | if (!UIMethods.empty()) |
| 114 | return; |
| 115 | |
| 116 | // TODO: This should eventually be a comprehensive list of UIKit methods |
| 117 | |
| 118 | // UILabel Methods |
| 119 | llvm::StringMap<uint8_t> &UILabelM = |
| 120 | UIMethods.insert({"UILabel", llvm::StringMap<uint8_t>()}).first->second; |
| 121 | UILabelM.insert({"setText:", 0}); |
| 122 | |
| 123 | // UIButton Methods |
| 124 | llvm::StringMap<uint8_t> &UIButtonM = |
| 125 | UIMethods.insert({"UIButton", llvm::StringMap<uint8_t>()}).first->second; |
| 126 | UIButtonM.insert({"setText:", 0}); |
| 127 | |
| 128 | // UIAlertAction Methods |
| 129 | llvm::StringMap<uint8_t> &UIAlertActionM = |
| 130 | UIMethods.insert({"UIAlertAction", llvm::StringMap<uint8_t>()}) |
| 131 | .first->second; |
| 132 | UIAlertActionM.insert({"actionWithTitle:style:handler:", 0}); |
| 133 | |
| 134 | // UIAlertController Methods |
| 135 | llvm::StringMap<uint8_t> &UIAlertControllerM = |
| 136 | UIMethods.insert({"UIAlertController", llvm::StringMap<uint8_t>()}) |
| 137 | .first->second; |
| 138 | UIAlertControllerM.insert( |
| 139 | {"alertControllerWithTitle:message:preferredStyle:", 1}); |
| 140 | |
| 141 | // NSButton Methods |
| 142 | llvm::StringMap<uint8_t> &NSButtonM = |
| 143 | UIMethods.insert({"NSButton", llvm::StringMap<uint8_t>()}).first->second; |
| 144 | NSButtonM.insert({"setTitle:", 0}); |
| 145 | |
| 146 | // NSButtonCell Methods |
| 147 | llvm::StringMap<uint8_t> &NSButtonCellM = |
| 148 | UIMethods.insert({"NSButtonCell", llvm::StringMap<uint8_t>()}) |
| 149 | .first->second; |
| 150 | NSButtonCellM.insert({"setTitle:", 0}); |
| 151 | |
| 152 | // NSMenuItem Methods |
| 153 | llvm::StringMap<uint8_t> &NSMenuItemM = |
| 154 | UIMethods.insert({"NSMenuItem", llvm::StringMap<uint8_t>()}) |
| 155 | .first->second; |
| 156 | NSMenuItemM.insert({"setTitle:", 0}); |
| 157 | |
| 158 | // NSAttributedString Methods |
| 159 | llvm::StringMap<uint8_t> &NSAttributedStringM = |
| 160 | UIMethods.insert({"NSAttributedString", llvm::StringMap<uint8_t>()}) |
| 161 | .first->second; |
| 162 | NSAttributedStringM.insert({"initWithString:", 0}); |
| 163 | NSAttributedStringM.insert({"initWithString:attributes:", 0}); |
| 164 | } |
| 165 | |
| 166 | /// Initializes a list of methods and C functions that return a localized string |
| 167 | void NonLocalizedStringChecker::initLocStringsMethods(ASTContext &Ctx) const { |
| 168 | if (!LSM.empty()) |
| 169 | return; |
| 170 | |
| 171 | LSM.insert({"NSBundle", "localizedStringForKey:value:table:"}); |
| 172 | LSM.insert({"NSDateFormatter", "stringFromDate:"}); |
| 173 | LSM.insert( |
| 174 | {"NSDateFormatter", "localizedStringFromDate:dateStyle:timeStyle:"}); |
| 175 | LSM.insert({"NSNumberFormatter", "stringFromNumber:"}); |
| 176 | LSM.insert({"UITextField", "text"}); |
| 177 | LSM.insert({"UITextView", "text"}); |
| 178 | LSM.insert({"UILabel", "text"}); |
| 179 | |
| 180 | LSF.insert("CFDateFormatterCreateStringWithDate"); |
| 181 | LSF.insert("CFDateFormatterCreateStringWithAbsoluteTime"); |
| 182 | LSF.insert("CFNumberFormatterCreateStringWithNumber"); |
| 183 | } |
| 184 | |
| 185 | /// Checks to see if the method / function declaration includes |
| 186 | /// __attribute__((annotate("returns_localized_nsstring"))) |
| 187 | bool NonLocalizedStringChecker::isAnnotatedAsLocalized(const Decl *D) const { |
| 188 | return std::any_of( |
| 189 | D->specific_attr_begin<AnnotateAttr>(), |
| 190 | D->specific_attr_end<AnnotateAttr>(), [](const AnnotateAttr *Ann) { |
| 191 | return Ann->getAnnotation() == "returns_localized_nsstring"; |
| 192 | }); |
| 193 | } |
| 194 | |
| 195 | /// Returns true if the given SVal is marked as Localized in the program state |
| 196 | bool NonLocalizedStringChecker::hasLocalizedState(SVal S, |
| 197 | CheckerContext &C) const { |
| 198 | const MemRegion *mt = S.getAsRegion(); |
| 199 | if (mt) { |
| 200 | const LocalizedState *LS = C.getState()->get<LocalizedMemMap>(mt); |
| 201 | if (LS && LS->isLocalized()) |
| 202 | return true; |
| 203 | } |
| 204 | return false; |
| 205 | } |
| 206 | |
| 207 | /// Returns true if the given SVal is marked as NonLocalized in the program |
| 208 | /// state |
| 209 | bool NonLocalizedStringChecker::hasNonLocalizedState(SVal S, |
| 210 | CheckerContext &C) const { |
| 211 | const MemRegion *mt = S.getAsRegion(); |
| 212 | if (mt) { |
| 213 | const LocalizedState *LS = C.getState()->get<LocalizedMemMap>(mt); |
| 214 | if (LS && LS->isNonLocalized()) |
| 215 | return true; |
| 216 | } |
| 217 | return false; |
| 218 | } |
| 219 | |
| 220 | /// Marks the given SVal as Localized in the program state |
| 221 | void NonLocalizedStringChecker::setLocalizedState(const SVal S, |
| 222 | CheckerContext &C) const { |
| 223 | const MemRegion *mt = S.getAsRegion(); |
| 224 | if (mt) { |
| 225 | ProgramStateRef State = |
| 226 | C.getState()->set<LocalizedMemMap>(mt, LocalizedState::getLocalized()); |
| 227 | C.addTransition(State); |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | /// Marks the given SVal as NonLocalized in the program state |
| 232 | void NonLocalizedStringChecker::setNonLocalizedState(const SVal S, |
| 233 | CheckerContext &C) const { |
| 234 | const MemRegion *mt = S.getAsRegion(); |
| 235 | if (mt) { |
| 236 | ProgramStateRef State = C.getState()->set<LocalizedMemMap>( |
| 237 | mt, LocalizedState::getNonLocalized()); |
| 238 | C.addTransition(State); |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | /// Reports a localization error for the passed in method call and SVal |
| 243 | void NonLocalizedStringChecker::reportLocalizationError( |
| 244 | SVal S, const ObjCMethodCall &M, CheckerContext &C, |
| 245 | int argumentNumber) const { |
| 246 | |
| 247 | ExplodedNode *ErrNode = C.getPredecessor(); |
| 248 | static CheckerProgramPointTag Tag("NonLocalizedStringChecker", |
| 249 | "UnlocalizedString"); |
| 250 | ErrNode = C.addTransition(C.getState(), C.getPredecessor(), &Tag); |
| 251 | |
| 252 | if (!ErrNode) |
| 253 | return; |
| 254 | |
| 255 | // Generate the bug report. |
| 256 | std::unique_ptr<BugReport> R( |
| 257 | new BugReport(*BT, "String should be localized", ErrNode)); |
| 258 | if (argumentNumber) { |
| 259 | R->addRange(M.getArgExpr(argumentNumber - 1)->getSourceRange()); |
| 260 | } else { |
| 261 | R->addRange(M.getSourceRange()); |
| 262 | } |
| 263 | R->markInteresting(S); |
| 264 | C.emitReport(std::move(R)); |
| 265 | } |
| 266 | |
| 267 | /// Check if the string being passed in has NonLocalized state |
| 268 | void NonLocalizedStringChecker::checkPreObjCMessage(const ObjCMethodCall &msg, |
| 269 | CheckerContext &C) const { |
| 270 | initUIMethods(C.getASTContext()); |
| 271 | |
| 272 | const ObjCInterfaceDecl *OD = msg.getReceiverInterface(); |
| 273 | if (!OD) |
| 274 | return; |
| 275 | const IdentifierInfo *odInfo = OD->getIdentifier(); |
| 276 | |
| 277 | Selector S = msg.getSelector(); |
| 278 | |
| 279 | std::string SelectorString = S.getAsString(); |
| 280 | StringRef SelectorName = SelectorString; |
| 281 | assert(!SelectorName.empty()); |
| 282 | |
| 283 | auto method = UIMethods.find(odInfo->getName()); |
| 284 | if (odInfo->isStr("NSString")) { |
| 285 | // Handle the case where the receiver is an NSString |
| 286 | // These special NSString methods draw to the screen |
| 287 | |
| 288 | if (!(SelectorName.startswith("drawAtPoint") || |
| 289 | SelectorName.startswith("drawInRect") || |
| 290 | SelectorName.startswith("drawWithRect"))) |
| 291 | return; |
| 292 | |
| 293 | SVal svTitle = msg.getReceiverSVal(); |
| 294 | |
| 295 | bool isNonLocalized = hasNonLocalizedState(svTitle, C); |
| 296 | |
| 297 | if (isNonLocalized) { |
| 298 | reportLocalizationError(svTitle, msg, C); |
| 299 | } |
| 300 | } else if (method != UIMethods.end()) { |
| 301 | |
| 302 | auto argumentIterator = method->getValue().find(SelectorName); |
| 303 | |
| 304 | if (argumentIterator == method->getValue().end()) |
| 305 | return; |
| 306 | |
| 307 | int argumentNumber = argumentIterator->getValue(); |
| 308 | |
| 309 | SVal svTitle = msg.getArgSVal(argumentNumber); |
| 310 | |
| 311 | if (const ObjCStringRegion *SR = |
| 312 | dyn_cast_or_null<ObjCStringRegion>(svTitle.getAsRegion())) { |
| 313 | StringRef stringValue = |
| 314 | SR->getObjCStringLiteral()->getString()->getString(); |
| 315 | if ((stringValue.trim().size() == 0 && stringValue.size() > 0) || |
| 316 | stringValue.empty()) |
| 317 | return; |
| 318 | if (!IsAggressive && llvm::sys::unicode::columnWidthUTF8(stringValue) < 2) |
| 319 | return; |
| 320 | } |
| 321 | |
| 322 | bool isNonLocalized = hasNonLocalizedState(svTitle, C); |
| 323 | |
| 324 | if (isNonLocalized) { |
| 325 | reportLocalizationError(svTitle, msg, C, argumentNumber + 1); |
| 326 | } |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | static inline bool isNSStringType(QualType T, ASTContext &Ctx) { |
| 331 | |
| 332 | const ObjCObjectPointerType *PT = T->getAs<ObjCObjectPointerType>(); |
| 333 | if (!PT) |
| 334 | return false; |
| 335 | |
| 336 | ObjCInterfaceDecl *Cls = PT->getObjectType()->getInterface(); |
| 337 | if (!Cls) |
| 338 | return false; |
| 339 | |
| 340 | IdentifierInfo *ClsName = Cls->getIdentifier(); |
| 341 | |
| 342 | // FIXME: Should we walk the chain of classes? |
| 343 | return ClsName == &Ctx.Idents.get("NSString") || |
| 344 | ClsName == &Ctx.Idents.get("NSMutableString"); |
| 345 | } |
| 346 | |
| 347 | /// Marks a string being returned by any call as localized |
| 348 | /// if it is in LocStringFunctions (LSF) or the function is annotated. |
| 349 | /// Otherwise, we mark it as NonLocalized (Aggressive) or |
| 350 | /// NonLocalized only if it is not backed by a SymRegion (Non-Aggressive), |
| 351 | /// basically leaving only string literals as NonLocalized. |
| 352 | void NonLocalizedStringChecker::checkPostCall(const CallEvent &Call, |
| 353 | CheckerContext &C) const { |
| 354 | initLocStringsMethods(C.getASTContext()); |
| 355 | |
| 356 | if (!Call.getOriginExpr()) |
| 357 | return; |
| 358 | |
| 359 | // Anything that takes in a localized NSString as an argument |
| 360 | // and returns an NSString will be assumed to be returning a |
| 361 | // localized NSString. (Counter: Incorrectly combining two LocalizedStrings) |
| 362 | const QualType RT = Call.getResultType(); |
| 363 | if (isNSStringType(RT, C.getASTContext())) { |
| 364 | for (unsigned i = 0; i < Call.getNumArgs(); ++i) { |
| 365 | SVal argValue = Call.getArgSVal(i); |
| 366 | if (hasLocalizedState(argValue, C)) { |
| 367 | SVal sv = Call.getReturnValue(); |
| 368 | setLocalizedState(sv, C); |
| 369 | return; |
| 370 | } |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | const Decl *D = Call.getDecl(); |
| 375 | if (!D) |
| 376 | return; |
| 377 | |
| 378 | StringRef IdentifierName = C.getCalleeName(D->getAsFunction()); |
| 379 | |
| 380 | SVal sv = Call.getReturnValue(); |
| 381 | if (isAnnotatedAsLocalized(D) || LSF.find(IdentifierName) != LSF.end()) { |
| 382 | setLocalizedState(sv, C); |
| 383 | } else if (isNSStringType(RT, C.getASTContext()) && |
| 384 | !hasLocalizedState(sv, C)) { |
| 385 | if (IsAggressive) { |
| 386 | setNonLocalizedState(sv, C); |
| 387 | } else { |
| 388 | const SymbolicRegion *SymReg = |
| 389 | dyn_cast_or_null<SymbolicRegion>(sv.getAsRegion()); |
| 390 | if (!SymReg) |
| 391 | setNonLocalizedState(sv, C); |
| 392 | } |
| 393 | } |
| 394 | } |
| 395 | |
| 396 | /// Marks a string being returned by an ObjC method as localized |
| 397 | /// if it is in LocStringMethods or the method is annotated |
| 398 | void NonLocalizedStringChecker::checkPostObjCMessage(const ObjCMethodCall &msg, |
| 399 | CheckerContext &C) const { |
| 400 | initLocStringsMethods(C.getASTContext()); |
| 401 | |
| 402 | if (!msg.isInstanceMessage()) |
| 403 | return; |
| 404 | |
| 405 | const ObjCInterfaceDecl *OD = msg.getReceiverInterface(); |
| 406 | if (!OD) |
| 407 | return; |
| 408 | const IdentifierInfo *odInfo = OD->getIdentifier(); |
| 409 | |
| 410 | StringRef IdentifierName = odInfo->getName(); |
| 411 | |
| 412 | Selector S = msg.getSelector(); |
| 413 | std::string SelectorName = S.getAsString(); |
| 414 | |
| 415 | std::pair<StringRef, StringRef> MethodDescription = {IdentifierName, |
| 416 | SelectorName}; |
| 417 | |
| 418 | if (LSM.count(MethodDescription) || isAnnotatedAsLocalized(msg.getDecl())) { |
| 419 | SVal sv = msg.getReturnValue(); |
| 420 | setLocalizedState(sv, C); |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | /// Marks all empty string literals as localized |
| 425 | void NonLocalizedStringChecker::checkPostStmt(const ObjCStringLiteral *SL, |
| 426 | CheckerContext &C) const { |
| 427 | SVal sv = C.getSVal(SL); |
| 428 | setNonLocalizedState(sv, C); |
| 429 | } |
| 430 | |
| 431 | namespace { |
| 432 | class EmptyLocalizationContextChecker |
| 433 | : public Checker<check::ASTDecl<ObjCImplementationDecl>> { |
| 434 | |
| 435 | // A helper class, which walks the AST |
| 436 | class MethodCrawler : public ConstStmtVisitor<MethodCrawler> { |
| 437 | const ObjCMethodDecl *MD; |
| 438 | BugReporter &BR; |
| 439 | AnalysisManager &Mgr; |
| 440 | const CheckerBase *Checker; |
| 441 | LocationOrAnalysisDeclContext DCtx; |
| 442 | |
| 443 | public: |
| 444 | MethodCrawler(const ObjCMethodDecl *InMD, BugReporter &InBR, |
| 445 | const CheckerBase *Checker, AnalysisManager &InMgr, |
| 446 | AnalysisDeclContext *InDCtx) |
| 447 | : MD(InMD), BR(InBR), Mgr(InMgr), Checker(Checker), DCtx(InDCtx) {} |
| 448 | |
| 449 | void VisitStmt(const Stmt *S) { VisitChildren(S); } |
| 450 | |
| 451 | void VisitObjCMessageExpr(const ObjCMessageExpr *ME); |
| 452 | |
| 453 | void reportEmptyContextError(const ObjCMessageExpr *M) const; |
| 454 | |
| 455 | void VisitChildren(const Stmt *S) { |
| 456 | for (const Stmt *Child : S->children()) { |
| 457 | if (Child) |
| 458 | this->Visit(Child); |
| 459 | } |
| 460 | } |
| 461 | }; |
| 462 | |
| 463 | public: |
| 464 | void checkASTDecl(const ObjCImplementationDecl *D, AnalysisManager &Mgr, |
| 465 | BugReporter &BR) const; |
| 466 | }; |
| 467 | } // end anonymous namespace |
| 468 | |
| 469 | void EmptyLocalizationContextChecker::checkASTDecl( |
| 470 | const ObjCImplementationDecl *D, AnalysisManager &Mgr, |
| 471 | BugReporter &BR) const { |
| 472 | |
| 473 | for (const ObjCMethodDecl *M : D->methods()) { |
| 474 | AnalysisDeclContext *DCtx = Mgr.getAnalysisDeclContext(M); |
| 475 | |
| 476 | const Stmt *Body = M->getBody(); |
| 477 | assert(Body); |
| 478 | |
| 479 | MethodCrawler MC(M->getCanonicalDecl(), BR, this, Mgr, DCtx); |
| 480 | MC.VisitStmt(Body); |
| 481 | } |
| 482 | } |
| 483 | |
| 484 | /// This check attempts to match these macros, assuming they are defined as |
| 485 | /// follows: |
| 486 | /// |
| 487 | /// #define NSLocalizedString(key, comment) \ |
| 488 | /// [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil] |
| 489 | /// #define NSLocalizedStringFromTable(key, tbl, comment) \ |
| 490 | /// [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)] |
| 491 | /// #define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \ |
| 492 | /// [bundle localizedStringForKey:(key) value:@"" table:(tbl)] |
| 493 | /// #define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) |
| 494 | /// |
| 495 | /// We cannot use the path sensitive check because the macro argument we are |
| 496 | /// checking for (comment) is not used and thus not present in the AST, |
| 497 | /// so we use Lexer on the original macro call and retrieve the value of |
| 498 | /// the comment. If it's empty or nil, we raise a warning. |
| 499 | void EmptyLocalizationContextChecker::MethodCrawler::VisitObjCMessageExpr( |
| 500 | const ObjCMessageExpr *ME) { |
| 501 | |
| 502 | const ObjCInterfaceDecl *OD = ME->getReceiverInterface(); |
| 503 | if (!OD) |
| 504 | return; |
| 505 | |
| 506 | const IdentifierInfo *odInfo = OD->getIdentifier(); |
| 507 | |
| 508 | if (!(odInfo->isStr("NSBundle") || |
| 509 | ME->getSelector().getAsString() == |
| 510 | "localizedStringForKey:value:table:")) { |
| 511 | return; |
| 512 | } |
| 513 | |
| 514 | SourceRange R = ME->getSourceRange(); |
| 515 | if (!R.getBegin().isMacroID()) |
| 516 | return; |
| 517 | |
| 518 | // getImmediateMacroCallerLoc gets the location of the immediate macro |
| 519 | // caller, one level up the stack toward the initial macro typed into the |
| 520 | // source, so SL should point to the NSLocalizedString macro. |
| 521 | SourceLocation SL = |
| 522 | Mgr.getSourceManager().getImmediateMacroCallerLoc(R.getBegin()); |
| 523 | std::pair<FileID, unsigned> SLInfo = |
| 524 | Mgr.getSourceManager().getDecomposedLoc(SL); |
| 525 | |
| 526 | SrcMgr::SLocEntry SE = Mgr.getSourceManager().getSLocEntry(SLInfo.first); |
| 527 | |
| 528 | // If NSLocalizedString macro is wrapped in another macro, we need to |
| 529 | // unwrap the expansion until we get to the NSLocalizedStringMacro. |
| 530 | while (SE.isExpansion()) { |
| 531 | SL = SE.getExpansion().getSpellingLoc(); |
| 532 | SLInfo = Mgr.getSourceManager().getDecomposedLoc(SL); |
| 533 | SE = Mgr.getSourceManager().getSLocEntry(SLInfo.first); |
| 534 | } |
| 535 | |
| 536 | llvm::MemoryBuffer *BF = SE.getFile().getContentCache()->getRawBuffer(); |
| 537 | Lexer TheLexer(SL, LangOptions(), BF->getBufferStart(), |
| 538 | BF->getBufferStart() + SLInfo.second, BF->getBufferEnd()); |
| 539 | |
| 540 | Token I; |
| 541 | Token Result; // This will hold the token just before the last ')' |
| 542 | int p_count = 0; // This is for parenthesis matching |
| 543 | while (!TheLexer.LexFromRawLexer(I)) { |
| 544 | if (I.getKind() == tok::l_paren) |
| 545 | ++p_count; |
| 546 | if (I.getKind() == tok::r_paren) { |
| 547 | if (p_count == 1) |
| 548 | break; |
| 549 | --p_count; |
| 550 | } |
| 551 | Result = I; |
| 552 | } |
| 553 | |
| 554 | if (isAnyIdentifier(Result.getKind())) { |
| 555 | if (Result.getRawIdentifier().equals("nil")) { |
| 556 | reportEmptyContextError(ME); |
| 557 | return; |
| 558 | } |
| 559 | } |
| 560 | |
| 561 | if (!isStringLiteral(Result.getKind())) |
| 562 | return; |
| 563 | |
| 564 | StringRef Comment = |
| 565 | StringRef(Result.getLiteralData(), Result.getLength()).trim("\""); |
| 566 | |
| 567 | if ((Comment.trim().size() == 0 && Comment.size() > 0) || // Is Whitespace |
| 568 | Comment.empty()) { |
| 569 | reportEmptyContextError(ME); |
| 570 | } |
| 571 | } |
| 572 | |
| 573 | void EmptyLocalizationContextChecker::MethodCrawler::reportEmptyContextError( |
| 574 | const ObjCMessageExpr *ME) const { |
| 575 | // Generate the bug report. |
| 576 | BR.EmitBasicReport(MD, Checker, "Context Missing", "Localizability Error", |
| 577 | "Localized string macro should include a non-empty " |
| 578 | "comment for translators", |
| 579 | PathDiagnosticLocation(ME, BR.getSourceManager(), DCtx)); |
| 580 | } |
| 581 | |
| 582 | //===----------------------------------------------------------------------===// |
| 583 | // Checker registration. |
| 584 | //===----------------------------------------------------------------------===// |
| 585 | |
| 586 | void ento::registerNonLocalizedStringChecker(CheckerManager &mgr) { |
| 587 | NonLocalizedStringChecker *checker = |
| 588 | mgr.registerChecker<NonLocalizedStringChecker>(); |
| 589 | checker->IsAggressive = |
| 590 | mgr.getAnalyzerOptions().getBooleanOption("AggressiveReport", false); |
| 591 | } |
| 592 | |
| 593 | void ento::registerEmptyLocalizationContextChecker(CheckerManager &mgr) { |
| 594 | mgr.registerChecker<EmptyLocalizationContextChecker>(); |
Ted Kremenek | 9589caf | 2015-08-26 03:11:31 +0000 | [diff] [blame^] | 595 | } |