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 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<"Check that warns about uses of non-localized NSStrings passed to UI methods expecting localized strings">, + DescFile<"LocalizationChecker.cpp">; + +def EmptyLocalizationContextChecker : Checker<"EmptyLocalizationContextChecker">, + HelpText<"Check that NSLocalizedString includes a comment for context">, + DescFile<"LocalizationChecker.cpp">; } // end "alpha.osx.cocoa" Index: lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp =================================================================== --- /dev/null +++ lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp @@ -0,0 +1,522 @@ +//=- 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 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 { Unknown, 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> { + + mutable std::unique_ptr BT; + + mutable std::map> UIMethods; + mutable llvm::SmallSet, 10> LSM; + mutable llvm::StringMap LSF; // StringSet doesn't work + + 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) 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}}}, + {"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(), + D->specific_attr_end(), [](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(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(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(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( + 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) const { + + ExplodedNode *ErrNode = C.addTransition(); + if (!ErrNode) + return; + + // Generate the bug report. + std::unique_ptr R( + new BugReport(*BT, "String should be localized", ErrNode)); + 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 + StringRef drawAtPoint("drawAtPoint"); + StringRef drawInRect("drawInRect"); + StringRef drawWithRect("drawWithRect"); + + 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 m = method->second; + + auto argumentIterator = m.find(Name); + + if (argumentIterator == m.end()) + return; + + int argumentNumber = argumentIterator->second; + + SVal svTitle = msg.getArgSVal(argumentNumber); + + bool isNonLocalized = hasNonLocalizedState(svTitle, C); + + if (isNonLocalized) { + reportLocalizationError(svTitle, msg, C); + } + } +} + +static inline bool isNSStringType(QualType T, ASTContext &Ctx) { + const ObjCObjectPointerType *PT = T->getAs(); + 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 an C function as localized +/// if it is in LocStringFunctions (LSF) or the function is annotated +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 { + // Perhaps I can use a different heuristic for non-aggressive reporting? + const SymbolicRegion *SymReg = + dyn_cast_or_null(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 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 { + + StringRef stringValue = SL->getString()->getString(); + SVal sv = C.getSVal(SL); + if (stringValue.empty()) { + setLocalizedState(sv, C); + } else { + setNonLocalizedState(sv, C); + } +} + +namespace { +class EmptyLocalizationContextChecker + : public Checker> { + + // A helper class, which walks the AST + class MethodCrawler : public ConstStmtVisitor { + 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); + } +} + +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 will return the Loc exactly one expansion up + // towards what's written in code so in this case SL should always point to + // an NSLocalizedString macro + SourceLocation SL = + Mgr.getSourceManager().getImmediateMacroCallerLoc(R.getBegin()); + std::pair SLInfo = + Mgr.getSourceManager().getDecomposedLoc(SL); + + SrcMgr::SLocEntry SE = Mgr.getSourceManager().getSLocEntry(SLInfo.first); + + 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; + while (!TheLexer.LexFromRawLexer(I)) { + if (I.getKind() == tok::l_paren) + ++p_count; + if (I.getKind() == tok::r_paren) { + if (p_count == 1) // A ghetto solution for parenthesis matching + 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 comment for translator", + PathDiagnosticLocation(ME, BR.getSourceManager(), DCtx)); +} + +//===----------------------------------------------------------------------===// +// Checker registration. +//===----------------------------------------------------------------------===// + +void ento::registerNonLocalizedStringChecker(CheckerManager &mgr) { + NonLocalizedStringChecker *checker = + mgr.registerChecker(); + checker->IsAggressive = + mgr.getAnalyzerOptions().getBooleanOption("AggressiveReport", false); +} + +void ento::registerEmptyLocalizationContextChecker(CheckerManager &mgr) { + mgr.registerChecker(); +} Index: test/Analysis/localization-aggressive.m =================================================================== --- /dev/null +++ test/Analysis/localization-aggressive.m @@ -0,0 +1,265 @@ +// 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 comment for translator}} + NSString *string2 = NSLocalizedString( + @"LocalizedString", nil); // expected-warning {{Localized string macro + // should include comment for translator}} + NSString *string3 = NSLocalizedString( + @"LocalizedString", nil); // expected-warning {{Localized string macro + // should include comment for translator}} +} + +- (void)testEmptyLocalizationContext { + NSString *string = NSLocalizedString( + @"LocalizedString", @""); // expected-warning {{Localized string macro + // should include comment for translator}} + NSString *string2 = NSLocalizedString( + @"LocalizedString", @" "); // expected-warning {{Localized string macro + // should include comment for translator}} + NSString *string3 = NSLocalizedString( + @"LocalizedString", @" "); // expected-warning {{Localized string macro + // should include comment for translator}} +} + +- (void)testNSLocalizedStringVariants { + NSString *string = NSLocalizedStringFromTable( + @"LocalizedString", nil, @""); // expected-warning {{Localized string + // macro should include comment for + // translator}} + NSString *string2 = NSLocalizedStringFromTableInBundle( + @"LocalizedString", nil, [[NSBundle alloc] init], + @""); // expected-warning {{Localized string macro should include comment + // for translator}} + NSString *string3 = NSLocalizedStringWithDefaultValue( + @"LocalizedString", nil, [[NSBundle alloc] init], nil, + @""); // expected-warning {{Localized string macro should include comment + // for translator}} +} + +- (void)testMacroExpansionNilString { + NSString *string = YOLOC(@"Hello"); // expected-warning {{Localized string + // macro should include comment for + // translator}} + NSString *string2 = HOM(@"Hello"); // expected-warning {{Localized string + // macro should include comment for + // translator}} + NSString *string3 = NSLocalizedString((0 ? @"Critical" : @"Current"), + nil); // expected-warning {{Localized + // string macro should include + // comment for translator}} +} + +@end Index: test/Analysis/localization.m =================================================================== --- /dev/null +++ test/Analysis/localization.m @@ -0,0 +1,66 @@ +// 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