blob: 1d7a12c04610ffcc5332b612a5319abeb77d8862 [file] [log] [blame]
Anna Zakse5d74ca2015-08-14 20:22:22 +00001//=- 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
36using namespace clang;
37using namespace ento;
38
39namespace {
40struct LocalizedState {
41private:
42 enum Kind { NonLocalized, Localized } K;
43 LocalizedState(Kind InK) : K(InK) {}
44
45public:
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
61class 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
87public:
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
103REGISTER_MAP_WITH_PROGRAMSTATE(LocalizedMemMap, const MemRegion *,
104 LocalizedState)
105
106NonLocalizedStringChecker::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#}, ...}, ...}
112void 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
167void 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")))
187bool 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
196bool 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
209bool 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
221void 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
232void 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
243void 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
268void 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
330static 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.
352void 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
398void 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
425void NonLocalizedStringChecker::checkPostStmt(const ObjCStringLiteral *SL,
426 CheckerContext &C) const {
427 SVal sv = C.getSVal(SL);
428 setNonLocalizedState(sv, C);
429}
430
431namespace {
432class 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
463public:
464 void checkASTDecl(const ObjCImplementationDecl *D, AnalysisManager &Mgr,
465 BugReporter &BR) const;
466};
467} // end anonymous namespace
468
469void 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.
499void 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
573void 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
586void ento::registerNonLocalizedStringChecker(CheckerManager &mgr) {
587 NonLocalizedStringChecker *checker =
588 mgr.registerChecker<NonLocalizedStringChecker>();
589 checker->IsAggressive =
590 mgr.getAnalyzerOptions().getBooleanOption("AggressiveReport", false);
591}
592
593void ento::registerEmptyLocalizationContextChecker(CheckerManager &mgr) {
594 mgr.registerChecker<EmptyLocalizationContextChecker>();
Ted Kremenek9589caf2015-08-26 03:11:31 +0000595}