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>(); 595 } 596