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