kulpreet updated this revision to Diff 31181.
kulpreet added a comment.

- Added in a test for the case for macro expansion scenario that Anna pointed 
out.
- Skipping warning on string literals that only contain whitespace in addition 
to empty string literals.
- Ran on a large projects to verify low false-positive rate (will send details 
in a separate email to reviewers)


http://reviews.llvm.org/D11572

Files:
  lib/StaticAnalyzer/Checkers/CMakeLists.txt
  lib/StaticAnalyzer/Checkers/Checkers.td
  lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp
  test/Analysis/localization-aggressive.m
  test/Analysis/localization.m

Index: test/Analysis/localization.m
===================================================================
--- /dev/null
+++ test/Analysis/localization.m
@@ -0,0 +1,64 @@
+// RUN: %clang_cc1 -analyze -fblocks -analyzer-store=region -analyzer-checker=alpha.osx.cocoa.NonLocalizedStringChecker -analyzer-checker=alpha.osx.cocoa.EmptyLocalizationContextChecker -verify  %s
+
+// The larger set of tests in located in localization.m. These are tests
+// specific for non-aggressive reporting.
+
+// These declarations were reduced using Delta-Debugging from Foundation.h
+// on Mac OS X.
+
+#define nil ((id)0)
+#define NSLocalizedString(key, comment)                                        \
+  [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]
+#define NSLocalizedStringFromTable(key, tbl, comment)                          \
+  [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)]
+#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment)          \
+  [bundle localizedStringForKey:(key) value:@"" table:(tbl)]
+#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)      \
+  [bundle localizedStringForKey:(key) value:(val) table:(tbl)]
+@interface NSObject
++ (id)alloc;
+- (id)init;
+@end
+@interface NSString : NSObject
+@end
+@interface NSBundle : NSObject
++ (NSBundle *)mainBundle;
+- (NSString *)localizedStringForKey:(NSString *)key
+                              value:(NSString *)value
+                              table:(NSString *)tableName;
+@end
+@interface UILabel : NSObject
+@property(nullable, nonatomic, copy) NSString *text;
+@end
+@interface TestObject : NSObject
+@property(strong) NSString *text;
+@end
+
+@interface LocalizationTestSuite : NSObject
+int random();
+@end
+
+// Test cases begin here
+@implementation LocalizationTestSuite
+
+// An object passed in as an parameter's string member
+// should not be considered unlocalized
+- (void)testObjectAsArgument:(TestObject *)argumentObject {
+  UILabel *testLabel = [[UILabel alloc] init];
+
+  [testLabel setText:[argumentObject text]]; // no-warning
+  [testLabel setText:argumentObject.text];   // no-warning
+}
+
+- (void)testLocalizationErrorDetectedOnPathway {
+  UILabel *testLabel = [[UILabel alloc] init];
+  NSString *bar = NSLocalizedString(@"Hello", @"Comment");
+
+  if (random()) {
+    bar = @"Unlocalized string";
+  }
+
+  [testLabel setText:bar]; // expected-warning {{String should be localized}}
+}
+
+@end
Index: test/Analysis/localization-aggressive.m
===================================================================
--- /dev/null
+++ test/Analysis/localization-aggressive.m
@@ -0,0 +1,237 @@
+// RUN: %clang_cc1 -analyze -fblocks -analyzer-store=region  -analyzer-checker=alpha.osx.cocoa.NonLocalizedStringChecker -analyzer-checker=alpha.osx.cocoa.EmptyLocalizationContextChecker -verify  -analyzer-config AggressiveReport=true %s
+
+// These declarations were reduced using Delta-Debugging from Foundation.h
+// on Mac OS X.
+
+#define nil ((id)0)
+#define NSLocalizedString(key, comment)                                        \
+  [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]
+#define NSLocalizedStringFromTable(key, tbl, comment)                          \
+  [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)]
+#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment)          \
+  [bundle localizedStringForKey:(key) value:@"" table:(tbl)]
+#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)      \
+  [bundle localizedStringForKey:(key) value:(val) table:(tbl)]
+#define CGFLOAT_TYPE double
+typedef CGFLOAT_TYPE CGFloat;
+struct CGPoint {
+  CGFloat x;
+  CGFloat y;
+};
+typedef struct CGPoint CGPoint;
+@interface NSObject
++ (id)alloc;
+- (id)init;
+@end
+@class NSDictionary;
+@interface NSString : NSObject
+- (void)drawAtPoint:(CGPoint)point withAttributes:(NSDictionary *)attrs;
++ (instancetype)localizedStringWithFormat:(NSString *)format, ...;
+@end
+@interface NSBundle : NSObject
++ (NSBundle *)mainBundle;
+- (NSString *)localizedStringForKey:(NSString *)key
+                              value:(NSString *)value
+                              table:(NSString *)tableName;
+@end
+@interface UILabel : NSObject
+@property(nullable, nonatomic, copy) NSString *text;
+- (void)accessibilitySetIdentification:(NSString *)ident;
+@end
+@interface TestObject : NSObject
+@property(strong) NSString *text;
+@end
+
+@interface LocalizationTestSuite : NSObject
+NSString *ForceLocalized(NSString *str)
+    __attribute__((annotate("returns_localized_nsstring")));
+CGPoint CGPointMake(CGFloat x, CGFloat y);
+int random();
+// This next one is a made up API
+NSString *CFNumberFormatterCreateStringWithNumber(float x);
++ (NSString *)forceLocalized:(NSString *)str
+    __attribute__((annotate("returns_localized_nsstring")));
+@end
+
+// Test cases begin here
+@implementation LocalizationTestSuite
+
+// A C-Funtion that returns a localized string because it has the
+// "returns_localized_nsstring" annotation
+NSString *ForceLocalized(NSString *str) { return str; }
+// An ObjC method that returns a localized string because it has the
+// "returns_localized_nsstring" annotation
++ (NSString *)forceLocalized:(NSString *)str {
+  return str;
+}
+
+// An ObjC method that returns a localized string
++ (NSString *)unLocalizedStringMethod {
+  return @"UnlocalizedString";
+}
+
+- (void)testLocalizationErrorDetectedOnPathway {
+  UILabel *testLabel = [[UILabel alloc] init];
+  NSString *bar = NSLocalizedString(@"Hello", @"Comment");
+
+  if (random()) {
+    bar = @"Unlocalized string";
+  }
+
+  [testLabel setText:bar]; // expected-warning {{String should be localized}}
+}
+
+- (void)testLocalizationErrorDetectedOnNSString {
+  NSString *bar = NSLocalizedString(@"Hello", @"Comment");
+
+  if (random()) {
+    bar = @"Unlocalized string";
+  }
+
+  [bar drawAtPoint:CGPointMake(0, 0) withAttributes:nil]; // expected-warning {{String should be localized}}
+}
+
+- (void)testNoLocalizationErrorDetectedFromCFunction {
+  UILabel *testLabel = [[UILabel alloc] init];
+  NSString *bar = CFNumberFormatterCreateStringWithNumber(1);
+
+  [testLabel setText:bar]; // no-warning
+}
+
+- (void)testAnnotationAddsLocalizedStateForCFunction {
+  UILabel *testLabel = [[UILabel alloc] init];
+  NSString *bar = NSLocalizedString(@"Hello", @"Comment");
+
+  if (random()) {
+    bar = @"Unlocalized string";
+  }
+
+  [testLabel setText:ForceLocalized(bar)]; // no-warning
+}
+
+- (void)testAnnotationAddsLocalizedStateForObjCMethod {
+  UILabel *testLabel = [[UILabel alloc] init];
+  NSString *bar = NSLocalizedString(@"Hello", @"Comment");
+
+  if (random()) {
+    bar = @"Unlocalized string";
+  }
+
+  [testLabel setText:[LocalizationTestSuite forceLocalized:bar]]; // no-warning
+}
+
+// An empty string literal @"" should not raise an error
+- (void)testEmptyStringLiteralHasLocalizedState {
+  UILabel *testLabel = [[UILabel alloc] init];
+  NSString *bar = @"";
+
+  [testLabel setText:bar]; // no-warning
+}
+
+// An empty string literal @"" inline should not raise an error
+- (void)testInlineEmptyStringLiteralHasLocalizedState {
+  UILabel *testLabel = [[UILabel alloc] init];
+  [testLabel setText:@""]; // no-warning
+}
+
+// An string literal @"Hello" inline should raise an error
+- (void)testInlineStringLiteralHasLocalizedState {
+  UILabel *testLabel = [[UILabel alloc] init];
+  [testLabel setText:@"Hello"]; // expected-warning {{String should be localized}}
+}
+
+// A nil string should not raise an error
+- (void)testNilStringIsNotMarkedAsUnlocalized {
+  UILabel *testLabel = [[UILabel alloc] init];
+  [testLabel setText:nil]; // no-warning
+}
+
+// A method that takes in a localized string and returns a string
+// most likely that string is localized.
+- (void)testLocalizedStringArgument {
+  UILabel *testLabel = [[UILabel alloc] init];
+  NSString *localizedString = NSLocalizedString(@"Hello", @"Comment");
+
+  NSString *combinedString =
+      [NSString localizedStringWithFormat:@"%@", localizedString];
+
+  [testLabel setText:combinedString]; // no-warning
+}
+
+// A String passed in as a an parameter should not be considered
+// unlocalized
+- (void)testLocalizedStringAsArgument:(NSString *)argumentString {
+  UILabel *testLabel = [[UILabel alloc] init];
+
+  [testLabel setText:argumentString]; // no-warning
+}
+
+// A String passed into another method that calls a method that
+// requires a localized string should give an error
+- (void)localizedStringAsArgument:(NSString *)argumentString {
+  UILabel *testLabel = [[UILabel alloc] init];
+
+  [testLabel setText:argumentString]; // expected-warning {{String should be localized}}
+}
+
+// The warning is expected to be seen in localizedStringAsArgument: body
+- (void)testLocalizedStringAsArgumentOtherMethod:(NSString *)argumentString {
+  [self localizedStringAsArgument:@"UnlocalizedString"];
+}
+
+// [LocalizationTestSuite unLocalizedStringMethod] returns an unlocalized string
+// so we expect an error. Unfrtunately, it probably doesn't make a difference
+// what [LocalizationTestSuite unLocalizedStringMethod] returns since all
+// string values returned are marked as Unlocalized in aggressive reporting.
+- (void)testUnLocalizedStringMethod {
+  UILabel *testLabel = [[UILabel alloc] init];
+  NSString *bar = NSLocalizedString(@"Hello", @"Comment");
+
+  [testLabel setText:[LocalizationTestSuite unLocalizedStringMethod]]; // expected-warning {{String should be localized}}
+}
+
+// This is the reverse situation: accessibilitySetIdentification: doesn't care
+// about localization so we don't expect a warning
+- (void)testMethodNotInRequiresLocalizedStringMethods {
+  UILabel *testLabel = [[UILabel alloc] init];
+
+  [testLabel accessibilitySetIdentification:@"UnlocalizedString"]; // no-warning
+}
+
+// EmptyLocalizationContextChecker tests
+#define HOM(s) YOLOC(s)
+#define YOLOC(x) NSLocalizedString(x, nil)
+
+- (void)testNilLocalizationContext {
+  NSString *string = NSLocalizedString(@"LocalizedString", nil); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+  NSString *string2 = NSLocalizedString(@"LocalizedString", nil); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+  NSString *string3 = NSLocalizedString(@"LocalizedString", nil); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+}
+
+- (void)testEmptyLocalizationContext {
+  NSString *string = NSLocalizedString(@"LocalizedString", @""); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+  NSString *string2 = NSLocalizedString(@"LocalizedString", @" "); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+  NSString *string3 = NSLocalizedString(@"LocalizedString", @"	 "); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+}
+
+- (void)testNSLocalizedStringVariants {
+  NSString *string = NSLocalizedStringFromTable(@"LocalizedString", nil, @""); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+  NSString *string2 = NSLocalizedStringFromTableInBundle(@"LocalizedString", nil, [[NSBundle alloc] init],@""); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+  NSString *string3 = NSLocalizedStringWithDefaultValue(@"LocalizedString", nil, [[NSBundle alloc] init], nil,@""); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+}
+
+- (void)testMacroExpansionNilString {
+  NSString *string = YOLOC(@"Hello"); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+  NSString *string2 = HOM(@"Hello");  // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+  NSString *string3 = NSLocalizedString((0 ? @"Critical" : @"Current"),nil); // expected-warning {{Localized string macro should include a non-empty comment for translators}}
+}
+
+#define KCLocalizedString(x,comment) NSLocalizedString(x, comment)
+
+- (void)testNoWarningForNilCommentPassedIntoOtherMacro {
+  NSString *string = KCLocalizedString(@"Hello",@""); // no-warning
+  NSString *string2 = KCLocalizedString(@"Hello",nil); // no-warning
+  NSString *string3 = KCLocalizedString(@"Hello",@"Comment"); // no-warning
+}
+
+@end
Index: lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp
===================================================================
--- /dev/null
+++ lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp
@@ -0,0 +1,557 @@
+//=- LocalizationChecker.cpp -------------------------------------*- C++ -*-==//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+//
+//  This file defines a set of checks for localizability including:
+//  1) A checker that warns about uses of non-localized NSStrings passed to
+//     UI methods expecting localized strings
+//  2) A syntactic checker that warns against the bad practice of
+//     not including a comment in NSLocalizedString macros.
+//
+//===----------------------------------------------------------------------===//
+
+#include "ClangSACheckers.h"
+#include "SelectorExtras.h"
+#include "clang/AST/Attr.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclObjC.h"
+#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
+#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
+#include "clang/StaticAnalyzer/Core/Checker.h"
+#include "clang/StaticAnalyzer/Core/CheckerManager.h"
+#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
+#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
+#include "clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h"
+#include "clang/Lex/Lexer.h"
+#include "clang/AST/StmtVisitor.h"
+
+using namespace clang;
+using namespace ento;
+
+namespace {
+struct LocalizedState {
+private:
+  enum Kind { NonLocalized, Localized } K;
+  LocalizedState(Kind InK) : K(InK) {}
+
+public:
+  bool isLocalized() const { return K == Localized; }
+  bool isNonLocalized() const { return K == NonLocalized; }
+
+  static LocalizedState getLocalized() { return LocalizedState(Localized); }
+  static LocalizedState getNonLocalized() {
+    return LocalizedState(NonLocalized);
+  }
+
+  // Overload the == operator
+  bool operator==(const LocalizedState &X) const { return K == X.K; }
+
+  // LLVMs equivalent of a hash function
+  void Profile(llvm::FoldingSetNodeID &ID) const { ID.AddInteger(K); }
+};
+
+class NonLocalizedStringChecker
+    : public Checker<check::PostCall, check::PreObjCMessage,
+                     check::PostObjCMessage,
+                     check::PostStmt<ObjCStringLiteral>> {
+
+  mutable std::unique_ptr<BugType> BT;
+
+  // Methods that require a localized string
+  mutable std::map<StringRef, std::map<StringRef, uint8_t>> UIMethods;
+  // Methods that return a localized string
+  mutable llvm::SmallSet<std::pair<StringRef, StringRef>, 10> LSM;
+  // C Functions that return a localized string
+  mutable llvm::StringMap<char> LSF;
+
+  void initUIMethods(ASTContext &Ctx) const;
+  void initLocStringsMethods(ASTContext &Ctx) const;
+
+  bool hasNonLocalizedState(SVal S, CheckerContext &C) const;
+  bool hasLocalizedState(SVal S, CheckerContext &C) const;
+  void setNonLocalizedState(SVal S, CheckerContext &C) const;
+  void setLocalizedState(SVal S, CheckerContext &C) const;
+
+  bool isAnnotatedAsLocalized(const Decl *D) const;
+  void reportLocalizationError(SVal S, const ObjCMethodCall &M,
+                               CheckerContext &C, int argumentNumber = 0) const;
+
+public:
+  NonLocalizedStringChecker();
+
+  // When this parameter is set to true, the checker assumes all
+  // methods that return NSStrings are unlocalized. Thus, more false
+  // positives will be reported.
+  DefaultBool IsAggressive;
+
+  void checkPreObjCMessage(const ObjCMethodCall &msg, CheckerContext &C) const;
+  void checkPostObjCMessage(const ObjCMethodCall &msg, CheckerContext &C) const;
+  void checkPostStmt(const ObjCStringLiteral *SL, CheckerContext &C) const;
+  void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
+};
+
+} // end anonymous namespace
+
+REGISTER_MAP_WITH_PROGRAMSTATE(LocalizedMemMap, const MemRegion *,
+                               LocalizedState)
+
+NonLocalizedStringChecker::NonLocalizedStringChecker() {
+  BT.reset(new BugType(this, "Unlocalized string", "Localization Error"));
+}
+
+/// Initializes a list of methods that require a localized string
+/// Format: {"ClassName", {{"selectorName:", LocStringArg#}, ...}, ...}
+void NonLocalizedStringChecker::initUIMethods(ASTContext &Ctx) const {
+  if (!UIMethods.empty())
+    return;
+
+  // TODO: This should eventually be a comprehensive list of UIKit methods
+
+  UIMethods = {{"UILabel", {{"setText:", 0}}},
+               {"UIButton", {{"setText:", 0}}},
+               {"NSButton", {{"setTitle:", 0}}},
+               {"NSButtonCell", {{"setTitle:", 0}}},
+               {"NSMenuItem", {{"setTitle:", 0}}},
+               {"UIAlertAction", {{"actionWithTitle:style:handler:", 0}}},
+               {"UIAlertController",
+                {{"alertControllerWithTitle:message:preferredStyle:", 1}}},
+               {"NSAttributedString",
+                {{"initWithString:", 0}, {"initWithString:attributes:", 0}}}};
+}
+
+/// Initializes a list of methods and C functions that return a localized string
+void NonLocalizedStringChecker::initLocStringsMethods(ASTContext &Ctx) const {
+  if (!LSM.empty())
+    return;
+
+  LSM.insert({"NSBundle", "localizedStringForKey:value:table:"});
+  LSM.insert({"NSDateFormatter", "stringFromDate:"});
+  LSM.insert(
+      {"NSDateFormatter", "localizedStringFromDate:dateStyle:timeStyle:"});
+  LSM.insert({"NSNumberFormatter", "stringFromNumber:"});
+  LSM.insert({"UITextField", "text"});
+  LSM.insert({"UITextView", "text"});
+  LSM.insert({"UILabel", "text"});
+
+  LSF.insert({"CFDateFormatterCreateStringWithDate", '\0'});
+  LSF.insert({"CFDateFormatterCreateStringWithAbsoluteTime", '\0'});
+  LSF.insert({"CFNumberFormatterCreateStringWithNumber", '\0'});
+}
+
+/// Checks to see if the method / function declaration includes
+/// __attribute__((annotate("returns_localized_nsstring")))
+bool NonLocalizedStringChecker::isAnnotatedAsLocalized(const Decl *D) const {
+  return std::any_of(
+      D->specific_attr_begin<AnnotateAttr>(),
+      D->specific_attr_end<AnnotateAttr>(), [](const AnnotateAttr *Ann) {
+        return Ann->getAnnotation() == "returns_localized_nsstring";
+      });
+}
+
+/// Returns true if the given SVal is marked as Localized in the program state
+bool NonLocalizedStringChecker::hasLocalizedState(SVal S,
+                                                  CheckerContext &C) const {
+  const MemRegion *mt = S.getAsRegion();
+  if (mt) {
+    const LocalizedState *LS = C.getState()->get<LocalizedMemMap>(mt);
+    if (LS && LS->isLocalized())
+      return true;
+  }
+  return false;
+}
+
+/// Returns true if the given SVal is marked as NonLocalized in the program
+/// state
+bool NonLocalizedStringChecker::hasNonLocalizedState(SVal S,
+                                                     CheckerContext &C) const {
+  const MemRegion *mt = S.getAsRegion();
+  if (mt) {
+    const LocalizedState *LS = C.getState()->get<LocalizedMemMap>(mt);
+    if (LS && LS->isNonLocalized())
+      return true;
+  }
+  return false;
+}
+
+/// Marks the given SVal as Localized in the program state
+void NonLocalizedStringChecker::setLocalizedState(const SVal S,
+                                                  CheckerContext &C) const {
+  const MemRegion *mt = S.getAsRegion();
+  if (mt) {
+    ProgramStateRef State =
+        C.getState()->set<LocalizedMemMap>(mt, LocalizedState::getLocalized());
+    C.addTransition(State);
+  }
+}
+
+/// Marks the given SVal as NonLocalized in the program state
+void NonLocalizedStringChecker::setNonLocalizedState(const SVal S,
+                                                     CheckerContext &C) const {
+  const MemRegion *mt = S.getAsRegion();
+  if (mt) {
+    ProgramStateRef State = C.getState()->set<LocalizedMemMap>(
+        mt, LocalizedState::getNonLocalized());
+    C.addTransition(State);
+  }
+}
+
+/// Reports a localization error for the passed in method call and SVal
+void NonLocalizedStringChecker::reportLocalizationError(
+    SVal S, const ObjCMethodCall &M, CheckerContext &C,
+    int argumentNumber) const {
+
+  ExplodedNode *ErrNode = C.getPredecessor();
+  static CheckerProgramPointTag Tag("LocalizationChecker", "UnlocalizedString");
+  ErrNode = C.addTransition(C.getState(), C.getPredecessor(), &Tag);
+
+  if (!ErrNode)
+    return;
+
+  // Generate the bug report.
+  std::unique_ptr<BugReport> R(
+      new BugReport(*BT, "String should be localized", ErrNode));
+  if (argumentNumber) {
+    R->addRange(M.getArgExpr(argumentNumber - 1)->getSourceRange());
+  } else {
+    R->addRange(M.getSourceRange());
+  }
+  R->markInteresting(S);
+  C.emitReport(std::move(R));
+}
+
+/// Check if the string being passed in has NonLocalized state
+void NonLocalizedStringChecker::checkPreObjCMessage(const ObjCMethodCall &msg,
+                                                    CheckerContext &C) const {
+  initUIMethods(C.getASTContext());
+
+  if (!msg.isInstanceMessage())
+    return;
+
+  const ObjCInterfaceDecl *OD = msg.getReceiverInterface();
+  if (!OD)
+    return;
+  const IdentifierInfo *odInfo = OD->getIdentifier();
+
+  Selector S = msg.getSelector();
+
+  StringRef Name(S.getAsString());
+  assert(!Name.empty());
+
+  auto method = UIMethods.find(odInfo->getName());
+  if (odInfo->isStr("NSString")) {
+    // Handle the case where the receiver is an NSString
+    // These special NSString methods draw to the screen
+
+    if (!(Name.startswith("drawAtPoint") || Name.startswith("drawInRect") ||
+          Name.startswith("drawWithRect")))
+      return;
+
+    SVal svTitle = msg.getReceiverSVal();
+
+    bool isNonLocalized = hasNonLocalizedState(svTitle, C);
+
+    if (isNonLocalized) {
+      reportLocalizationError(svTitle, msg, C);
+    }
+  } else if (method != UIMethods.end()) {
+
+    std::map<StringRef, uint8_t> m = method->second;
+
+    auto argumentIterator = m.find(Name);
+
+    if (argumentIterator == m.end())
+      return;
+
+    int argumentNumber = argumentIterator->second;
+
+    SVal svTitle = msg.getArgSVal(argumentNumber);
+
+    if (const ObjCStringRegion *SR =
+            dyn_cast_or_null<ObjCStringRegion>(svTitle.getAsRegion())) {
+      StringRef stringValue =
+          SR->getObjCStringLiteral()->getString()->getString();
+      if ((stringValue.trim().size() == 0 && stringValue.size() > 0) ||
+          stringValue.empty())
+        return;
+      if (!IsAggressive && stringValue.size() < 2)
+        return;
+    }
+
+    bool isNonLocalized = hasNonLocalizedState(svTitle, C);
+
+    if (isNonLocalized) {
+      reportLocalizationError(svTitle, msg, C, argumentNumber + 1);
+    }
+  }
+}
+
+static inline bool isNSStringType(QualType T, ASTContext &Ctx) {
+  const ObjCObjectPointerType *PT = T->getAs<ObjCObjectPointerType>();
+  if (!PT)
+    return false;
+
+  ObjCInterfaceDecl *Cls = PT->getObjectType()->getInterface();
+  if (!Cls)
+    return false;
+
+  IdentifierInfo *ClsName = Cls->getIdentifier();
+
+  // FIXME: Should we walk the chain of classes?
+  return ClsName == &Ctx.Idents.get("NSString") ||
+         ClsName == &Ctx.Idents.get("NSMutableString");
+}
+
+/// Marks a string being returned by any call as localized
+/// if it is in LocStringFunctions (LSF) or the function is annotated.
+/// Otherwise, we mark it as NonLocalized (Aggressive) or
+/// NonLocalized only if it is not backed by a SymRegion (Non-Aggressive),
+/// basically leaving only string literals as NonLocalized.
+void NonLocalizedStringChecker::checkPostCall(const CallEvent &Call,
+                                              CheckerContext &C) const {
+  initLocStringsMethods(C.getASTContext());
+
+  // Anything that takes in a localized NSString as an argument
+  // and returns an NSString will be assumed to be returning a
+  // localized NSString. (Counter: Incorrectly combining two LocalizedStrings)
+  const QualType RT = Call.getResultType();
+  if (isNSStringType(RT, C.getASTContext())) {
+    for (unsigned i = 0; i < Call.getNumArgs(); ++i) {
+      SVal argValue = Call.getArgSVal(i);
+      if (hasLocalizedState(argValue, C)) {
+        SVal sv = Call.getReturnValue();
+        setLocalizedState(sv, C);
+        return;
+      }
+    }
+  }
+
+  const Decl *D = Call.getDecl();
+  if (!D)
+    return;
+
+  StringRef IdentifierName = C.getCalleeName(D->getAsFunction());
+
+  SVal sv = Call.getReturnValue();
+  if (isAnnotatedAsLocalized(D) || LSF.find(IdentifierName) != LSF.end()) {
+    setLocalizedState(sv, C);
+  } else if (isNSStringType(RT, C.getASTContext()) &&
+             !hasLocalizedState(sv, C)) {
+    if (IsAggressive) {
+      setNonLocalizedState(sv, C);
+    } else {
+      const SymbolicRegion *SymReg =
+          dyn_cast_or_null<SymbolicRegion>(sv.getAsRegion());
+      if (!SymReg)
+        setNonLocalizedState(sv, C);
+    }
+  }
+}
+
+/// Marks a string being returned by an ObjC method as localized
+/// if it is in LocStringMethods or the method is annotated
+void NonLocalizedStringChecker::checkPostObjCMessage(const ObjCMethodCall &msg,
+                                                     CheckerContext &C) const {
+  initLocStringsMethods(C.getASTContext());
+
+  if (!msg.isInstanceMessage())
+    return;
+
+  const ObjCInterfaceDecl *OD = msg.getReceiverInterface();
+  if (!OD)
+    return;
+  const IdentifierInfo *odInfo = OD->getIdentifier();
+
+  StringRef IdentifierName = odInfo->getName();
+
+  Selector S = msg.getSelector();
+  StringRef SelectorName = S.getAsString();
+  assert(!SelectorName.empty());
+
+  std::pair<StringRef, StringRef> MethodDescription = {IdentifierName,
+                                                       SelectorName};
+
+  if (LSM.count(MethodDescription) || isAnnotatedAsLocalized(msg.getDecl())) {
+    SVal sv = msg.getReturnValue();
+    setLocalizedState(sv, C);
+  }
+}
+
+/// Marks all empty string literals as localized
+void NonLocalizedStringChecker::checkPostStmt(const ObjCStringLiteral *SL,
+                                              CheckerContext &C) const {
+  SVal sv = C.getSVal(SL);
+  setNonLocalizedState(sv, C);
+}
+
+namespace {
+class EmptyLocalizationContextChecker
+    : public Checker<check::ASTDecl<ObjCImplementationDecl>> {
+
+  // A helper class, which walks the AST
+  class MethodCrawler : public ConstStmtVisitor<MethodCrawler> {
+    const ObjCMethodDecl *MD;
+    BugReporter &BR;
+    AnalysisManager &Mgr;
+    const CheckerBase *Checker;
+    LocationOrAnalysisDeclContext DCtx;
+
+  public:
+    MethodCrawler(const ObjCMethodDecl *InMD, BugReporter &InBR,
+                  const CheckerBase *Checker, AnalysisManager &InMgr,
+                  AnalysisDeclContext *InDCtx)
+        : MD(InMD), BR(InBR), Mgr(InMgr), Checker(Checker), DCtx(InDCtx) {}
+
+    void VisitStmt(const Stmt *S) { VisitChildren(S); }
+
+    void VisitObjCMessageExpr(const ObjCMessageExpr *ME);
+
+    void reportEmptyContextError(const ObjCMessageExpr *M) const;
+
+    void VisitChildren(const Stmt *S) {
+      for (const Stmt *Child : S->children()) {
+        if (Child)
+          this->Visit(Child);
+      }
+    }
+  };
+
+public:
+  void checkASTDecl(const ObjCImplementationDecl *D, AnalysisManager &Mgr,
+                    BugReporter &BR) const;
+};
+} // end anonymous namespace
+
+void EmptyLocalizationContextChecker::checkASTDecl(
+    const ObjCImplementationDecl *D, AnalysisManager &Mgr,
+    BugReporter &BR) const {
+
+  for (const ObjCMethodDecl *M : D->instance_methods()) {
+    AnalysisDeclContext *DCtx = Mgr.getAnalysisDeclContext(M);
+
+    const Stmt *Body = M->getBody();
+    assert(Body);
+
+    MethodCrawler MC(M->getCanonicalDecl(), BR, this, Mgr, DCtx);
+    MC.VisitStmt(Body);
+  }
+}
+
+/// This check attempts to match these macros, assuming they are defined as
+/// follows:
+///
+/// #define NSLocalizedString(key, comment) \
+/// [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]
+/// #define NSLocalizedStringFromTable(key, tbl, comment) \
+/// [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)]
+/// #define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
+/// [bundle localizedStringForKey:(key) value:@"" table:(tbl)]
+/// #define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)
+///
+/// We cannot use the path sensitive check because the macro argument we are
+/// checking for (comment) is not used and thus not present in the AST,
+/// so we use Lexer on the original macro call and retrieve the value of
+/// the comment. If it's empty or nil, we raise a warning.
+void EmptyLocalizationContextChecker::MethodCrawler::VisitObjCMessageExpr(
+    const ObjCMessageExpr *ME) {
+  if (!ME->isInstanceMessage())
+    return;
+
+  const ObjCInterfaceDecl *OD = ME->getReceiverInterface();
+  if (!OD)
+    return;
+
+  const IdentifierInfo *odInfo = OD->getIdentifier();
+
+  if (!(odInfo->isStr("NSBundle") ||
+        StringRef(ME->getSelector().getAsString())
+            .equals("localizedStringForKey:value:table:"))) {
+    return;
+  }
+
+  SourceRange R = ME->getSourceRange();
+  if (!R.getBegin().isMacroID())
+    return;
+
+  // getImmediateMacroCallerLoc gets the location of the immediate macro
+  // caller, one level up the stack toward the initial macro typed into the
+  // source, so SL should point to the NSLocalizedString macro.
+  SourceLocation SL =
+      Mgr.getSourceManager().getImmediateMacroCallerLoc(R.getBegin());
+  std::pair<FileID, unsigned> SLInfo =
+      Mgr.getSourceManager().getDecomposedLoc(SL);
+
+  SrcMgr::SLocEntry SE = Mgr.getSourceManager().getSLocEntry(SLInfo.first);
+
+  // If NSLocalizedString macro is wrapped in another macro, we need to
+  // unwrap the expansion until we get to the NSLocalizedStringMacro.
+  while (SE.isExpansion()) {
+    SL = SE.getExpansion().getSpellingLoc();
+    SLInfo = Mgr.getSourceManager().getDecomposedLoc(SL);
+    SE = Mgr.getSourceManager().getSLocEntry(SLInfo.first);
+  }
+
+  llvm::MemoryBuffer *BF = SE.getFile().getContentCache()->getRawBuffer();
+  Lexer TheLexer(SL, LangOptions(), BF->getBufferStart(),
+                 BF->getBufferStart() + SLInfo.second, BF->getBufferEnd());
+
+  Token I;
+  Token Result;    // This will hold the token just before the last ')'
+  int p_count = 0; // This is for parenthesis matching
+  while (!TheLexer.LexFromRawLexer(I)) {
+    if (I.getKind() == tok::l_paren)
+      ++p_count;
+    if (I.getKind() == tok::r_paren) {
+      if (p_count == 1)
+        break;
+      --p_count;
+    }
+    Result = I;
+  }
+
+  if (isAnyIdentifier(Result.getKind())) {
+    if (Result.getRawIdentifier().equals("nil")) {
+      reportEmptyContextError(ME);
+      return;
+    }
+  }
+
+  if (!isStringLiteral(Result.getKind()))
+    return;
+
+  StringRef Comment =
+      StringRef(Result.getLiteralData(), Result.getLength()).trim("\"");
+
+  if ((Comment.trim().size() == 0 && Comment.size() > 0) || // Is Whitespace
+      Comment.empty()) {
+    reportEmptyContextError(ME);
+  }
+}
+
+void EmptyLocalizationContextChecker::MethodCrawler::reportEmptyContextError(
+    const ObjCMessageExpr *ME) const {
+  // Generate the bug report.
+  BR.EmitBasicReport(MD, Checker, "Context Missing", "Localization Error",
+                     "Localized string macro should include a non-empty "
+                     "comment for translators",
+                     PathDiagnosticLocation(ME, BR.getSourceManager(), DCtx));
+}
+
+//===----------------------------------------------------------------------===//
+// Checker registration.
+//===----------------------------------------------------------------------===//
+
+void ento::registerNonLocalizedStringChecker(CheckerManager &mgr) {
+  NonLocalizedStringChecker *checker =
+      mgr.registerChecker<NonLocalizedStringChecker>();
+  checker->IsAggressive =
+      mgr.getAnalyzerOptions().getBooleanOption("AggressiveReport", false);
+}
+
+void ento::registerEmptyLocalizationContextChecker(CheckerManager &mgr) {
+  mgr.registerChecker<EmptyLocalizationContextChecker>();
+}
Index: lib/StaticAnalyzer/Checkers/Checkers.td
===================================================================
--- lib/StaticAnalyzer/Checkers/Checkers.td
+++ lib/StaticAnalyzer/Checkers/Checkers.td
@@ -455,6 +455,14 @@
 def ObjCGenericsChecker : Checker<"ObjCGenerics">,
   HelpText<"Checks for type errors.">,
   DescFile<"ObjCGenericsChecker.cpp">;
+  
+def NonLocalizedStringChecker : Checker<"NonLocalizedStringChecker">,
+  HelpText<"Warns about uses of non-localized NSStrings passed to UI methods expecting localized NSStrings">,
+  DescFile<"LocalizationChecker.cpp">;
+
+def EmptyLocalizationContextChecker : Checker<"EmptyLocalizationContextChecker">,
+  HelpText<"Check that NSLocalizedString macros include a comment for context">,
+  DescFile<"LocalizationChecker.cpp">;
 
 } // end "alpha.osx.cocoa"
 
Index: lib/StaticAnalyzer/Checkers/CMakeLists.txt
===================================================================
--- lib/StaticAnalyzer/Checkers/CMakeLists.txt
+++ lib/StaticAnalyzer/Checkers/CMakeLists.txt
@@ -39,6 +39,7 @@
   IdenticalExprChecker.cpp
   IvarInvalidationChecker.cpp
   LLVMConventionsChecker.cpp
+  LocalizationChecker.cpp
   MacOSKeychainAPIChecker.cpp
   MacOSXAPIChecker.cpp
   MallocChecker.cpp
_______________________________________________
cfe-commits mailing list
cfe-commits@cs.uiuc.edu
http://lists.cs.uiuc.edu/mailman/listinfo/cfe-commits

Reply via email to