mirror of
https://github.com/llvm/llvm-project.git
synced 2025-05-10 11:36:08 +00:00
595 lines
20 KiB
C++
595 lines
20 KiB
C++
![]() |
//=- 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"
|
||
|
#include "llvm/ADT/StringSet.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 llvm::StringMap<llvm::StringMap<uint8_t>> UIMethods;
|
||
|
// Methods that return a localized string
|
||
|
mutable llvm::SmallSet<std::pair<StringRef, StringRef>, 12> LSM;
|
||
|
// C Functions that return a localized string
|
||
|
mutable llvm::StringSet<> 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
|
||
|
|
||
|
// UILabel Methods
|
||
|
llvm::StringMap<uint8_t> &UILabelM =
|
||
|
UIMethods.insert({"UILabel", llvm::StringMap<uint8_t>()}).first->second;
|
||
|
UILabelM.insert({"setText:", 0});
|
||
|
|
||
|
// UIButton Methods
|
||
|
llvm::StringMap<uint8_t> &UIButtonM =
|
||
|
UIMethods.insert({"UIButton", llvm::StringMap<uint8_t>()}).first->second;
|
||
|
UIButtonM.insert({"setText:", 0});
|
||
|
|
||
|
// UIAlertAction Methods
|
||
|
llvm::StringMap<uint8_t> &UIAlertActionM =
|
||
|
UIMethods.insert({"UIAlertAction", llvm::StringMap<uint8_t>()})
|
||
|
.first->second;
|
||
|
UIAlertActionM.insert({"actionWithTitle:style:handler:", 0});
|
||
|
|
||
|
// UIAlertController Methods
|
||
|
llvm::StringMap<uint8_t> &UIAlertControllerM =
|
||
|
UIMethods.insert({"UIAlertController", llvm::StringMap<uint8_t>()})
|
||
|
.first->second;
|
||
|
UIAlertControllerM.insert(
|
||
|
{"alertControllerWithTitle:message:preferredStyle:", 1});
|
||
|
|
||
|
// NSButton Methods
|
||
|
llvm::StringMap<uint8_t> &NSButtonM =
|
||
|
UIMethods.insert({"NSButton", llvm::StringMap<uint8_t>()}).first->second;
|
||
|
NSButtonM.insert({"setTitle:", 0});
|
||
|
|
||
|
// NSButtonCell Methods
|
||
|
llvm::StringMap<uint8_t> &NSButtonCellM =
|
||
|
UIMethods.insert({"NSButtonCell", llvm::StringMap<uint8_t>()})
|
||
|
.first->second;
|
||
|
NSButtonCellM.insert({"setTitle:", 0});
|
||
|
|
||
|
// NSMenuItem Methods
|
||
|
llvm::StringMap<uint8_t> &NSMenuItemM =
|
||
|
UIMethods.insert({"NSMenuItem", llvm::StringMap<uint8_t>()})
|
||
|
.first->second;
|
||
|
NSMenuItemM.insert({"setTitle:", 0});
|
||
|
|
||
|
// NSAttributedString Methods
|
||
|
llvm::StringMap<uint8_t> &NSAttributedStringM =
|
||
|
UIMethods.insert({"NSAttributedString", llvm::StringMap<uint8_t>()})
|
||
|
.first->second;
|
||
|
NSAttributedStringM.insert({"initWithString:", 0});
|
||
|
NSAttributedStringM.insert({"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");
|
||
|
LSF.insert("CFDateFormatterCreateStringWithAbsoluteTime");
|
||
|
LSF.insert("CFNumberFormatterCreateStringWithNumber");
|
||
|
}
|
||
|
|
||
|
/// 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();
|
||
|
|
||
|
std::string SelectorString = S.getAsString();
|
||
|
StringRef SelectorName = SelectorString;
|
||
|
assert(!SelectorName.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 (!(SelectorName.startswith("drawAtPoint") ||
|
||
|
SelectorName.startswith("drawInRect") ||
|
||
|
SelectorName.startswith("drawWithRect")))
|
||
|
return;
|
||
|
|
||
|
SVal svTitle = msg.getReceiverSVal();
|
||
|
|
||
|
bool isNonLocalized = hasNonLocalizedState(svTitle, C);
|
||
|
|
||
|
if (isNonLocalized) {
|
||
|
reportLocalizationError(svTitle, msg, C);
|
||
|
}
|
||
|
} else if (method != UIMethods.end()) {
|
||
|
|
||
|
auto argumentIterator = method->getValue().find(SelectorName);
|
||
|
|
||
|
if (argumentIterator == method->getValue().end())
|
||
|
return;
|
||
|
|
||
|
int argumentNumber = argumentIterator->getValue();
|
||
|
|
||
|
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();
|
||
|
std::string SelectorName = S.getAsString();
|
||
|
|
||
|
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") ||
|
||
|
ME->getSelector().getAsString() ==
|
||
|
"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>();
|
||
|
}
|