[analyzer] Add checkers for OS X / iOS localizability issues

Add checkers that detect code-level localizability issues for OS X / iOS:
 - A path sensitive checker that warns about uses of non-localized
 NSStrings passed to UI methods expecting localized strings.
 - A syntax checker that warns against not including a comment in
 NSLocalizedString macros.

A patch by Kulpreet Chilana!

llvm-svn: 244389
This commit is contained in:
Anna Zaks 2015-08-08 01:49:26 +00:00
parent e44962e9bb
commit 38b496a05d
5 changed files with 897 additions and 0 deletions

View File

@ -39,6 +39,7 @@ add_clang_library(clangStaticAnalyzerCheckers
IdenticalExprChecker.cpp
IvarInvalidationChecker.cpp
LLVMConventionsChecker.cpp
LocalizationChecker.cpp
MacOSKeychainAPIChecker.cpp
MacOSXAPIChecker.cpp
MallocChecker.cpp

View File

@ -452,6 +452,14 @@ def DirectIvarAssignmentForAnnotatedFunctions : Checker<"DirectIvarAssignmentFor
HelpText<"Check for direct assignments to instance variables in the methods annotated with objc_no_direct_instance_variable_assignment">,
DescFile<"DirectIvarAssignment.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"
let ParentPackage = CoreFoundation in {

View File

@ -0,0 +1,559 @@
//=- 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/RecursiveASTVisitor.h"
#include "clang/AST/StmtVisitor.h"
#include "llvm/Support/Unicode.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", "Localizability 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("NonLocalizedStringChecker",
"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());
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 && llvm::sys::unicode::columnWidthUTF8(stringValue) < 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());
if (!Call.getOriginExpr())
return;
// 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->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) {
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", "Localizability 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>();
}

View File

@ -0,0 +1,243 @@
// 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)
#define POSSIBLE_FALSE_POSITIVE(s,other) KCLocalizedString(s,@"Comment")
- (void)testNoWarningForNilCommentPassedIntoOtherMacro {
NSString *string = KCLocalizedString(@"Hello",@""); // no-warning
NSString *string2 = KCLocalizedString(@"Hello",nil); // no-warning
NSString *string3 = KCLocalizedString(@"Hello",@"Comment"); // no-warning
}
- (void)testPossibleFalsePositiveSituationAbove {
NSString *string = POSSIBLE_FALSE_POSITIVE(@"Hello", nil); // no-warning
NSString *string2 = POSSIBLE_FALSE_POSITIVE(@"Hello", @"Hello"); // no-warning
}
@end

View File

@ -0,0 +1,86 @@
// 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}}
}
- (void)testOneCharacterStringsDoNotGiveAWarning {
UILabel *testLabel = [[UILabel alloc] init];
NSString *bar = NSLocalizedString(@"Hello", @"Comment");
if (random()) {
bar = @"-";
}
[testLabel setText:bar]; // no-warning
}
- (void)testOneCharacterUTFStringsDoNotGiveAWarning {
UILabel *testLabel = [[UILabel alloc] init];
NSString *bar = NSLocalizedString(@"Hello", @"Comment");
if (random()) {
bar = @"—";
}
[testLabel setText:bar]; // no-warning
}
@end