1417 lines · cpp
1//=- LocalizationChecker.cpp -------------------------------------*- C++ -*-==//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8//9// This file defines a set of checks for localizability including:10// 1) A checker that warns about uses of non-localized NSStrings passed to11// UI methods expecting localized strings12// 2) A syntactic checker that warns against the bad practice of13// not including a comment in NSLocalizedString macros.14//15//===----------------------------------------------------------------------===//16 17#include "clang/AST/Attr.h"18#include "clang/AST/Decl.h"19#include "clang/AST/DeclObjC.h"20#include "clang/AST/DynamicRecursiveASTVisitor.h"21#include "clang/AST/StmtVisitor.h"22#include "clang/Lex/Lexer.h"23#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"24#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"25#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"26#include "clang/StaticAnalyzer/Core/Checker.h"27#include "clang/StaticAnalyzer/Core/CheckerManager.h"28#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"29#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"30#include "clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h"31#include "llvm/ADT/STLExtras.h"32#include "llvm/Support/Unicode.h"33#include <optional>34 35using namespace clang;36using namespace ento;37 38namespace {39struct LocalizedState {40private:41 enum Kind { NonLocalized, Localized } K;42 LocalizedState(Kind InK) : K(InK) {}43 44public:45 bool isLocalized() const { return K == Localized; }46 bool isNonLocalized() const { return K == NonLocalized; }47 48 static LocalizedState getLocalized() { return LocalizedState(Localized); }49 static LocalizedState getNonLocalized() {50 return LocalizedState(NonLocalized);51 }52 53 // Overload the == operator54 bool operator==(const LocalizedState &X) const { return K == X.K; }55 56 // LLVMs equivalent of a hash function57 void Profile(llvm::FoldingSetNodeID &ID) const { ID.AddInteger(K); }58};59 60class NonLocalizedStringChecker61 : public Checker<check::PreCall, check::PostCall, check::PreObjCMessage,62 check::PostObjCMessage,63 check::PostStmt<ObjCStringLiteral>> {64 65 const BugType BT{this, "Unlocalizable string",66 "Localizability Issue (Apple)"};67 68 // Methods that require a localized string69 mutable llvm::DenseMap<const IdentifierInfo *,70 llvm::DenseMap<Selector, uint8_t>> UIMethods;71 // Methods that return a localized string72 mutable llvm::SmallSet<std::pair<const IdentifierInfo *, Selector>, 12> LSM;73 // C Functions that return a localized string74 mutable llvm::SmallPtrSet<const IdentifierInfo *, 5> LSF;75 76 void initUIMethods(ASTContext &Ctx) const;77 void initLocStringsMethods(ASTContext &Ctx) const;78 79 bool hasNonLocalizedState(SVal S, CheckerContext &C) const;80 bool hasLocalizedState(SVal S, CheckerContext &C) const;81 void setNonLocalizedState(SVal S, CheckerContext &C) const;82 void setLocalizedState(SVal S, CheckerContext &C) const;83 84 bool isAnnotatedAsReturningLocalized(const Decl *D) const;85 bool isAnnotatedAsTakingLocalized(const Decl *D) const;86 void reportLocalizationError(SVal S, const CallEvent &M, CheckerContext &C,87 int argumentNumber = 0) const;88 89 int getLocalizedArgumentForSelector(const IdentifierInfo *Receiver,90 Selector S) const;91 92public:93 // When this parameter is set to true, the checker assumes all94 // methods that return NSStrings are unlocalized. Thus, more false95 // positives will be reported.96 bool IsAggressive = false;97 98 void checkPreObjCMessage(const ObjCMethodCall &msg, CheckerContext &C) const;99 void checkPostObjCMessage(const ObjCMethodCall &msg, CheckerContext &C) const;100 void checkPostStmt(const ObjCStringLiteral *SL, CheckerContext &C) const;101 void checkPreCall(const CallEvent &Call, CheckerContext &C) const;102 void checkPostCall(const CallEvent &Call, CheckerContext &C) const;103};104 105} // end anonymous namespace106 107REGISTER_MAP_WITH_PROGRAMSTATE(LocalizedMemMap, const MemRegion *,108 LocalizedState)109 110namespace {111class NonLocalizedStringBRVisitor final : public BugReporterVisitor {112 113 const MemRegion *NonLocalizedString;114 bool Satisfied;115 116public:117 NonLocalizedStringBRVisitor(const MemRegion *NonLocalizedString)118 : NonLocalizedString(NonLocalizedString), Satisfied(false) {119 assert(NonLocalizedString);120 }121 122 PathDiagnosticPieceRef VisitNode(const ExplodedNode *Succ,123 BugReporterContext &BRC,124 PathSensitiveBugReport &BR) override;125 126 void Profile(llvm::FoldingSetNodeID &ID) const override {127 ID.Add(NonLocalizedString);128 }129};130} // End anonymous namespace.131 132#define NEW_RECEIVER(receiver) \133 llvm::DenseMap<Selector, uint8_t> &receiver##M = \134 UIMethods[&Ctx.Idents.get(#receiver)];135#define ADD_NULLARY_METHOD(receiver, method, argument) \136 receiver##M.insert( \137 {Ctx.Selectors.getNullarySelector(&Ctx.Idents.get(#method)), argument});138#define ADD_UNARY_METHOD(receiver, method, argument) \139 receiver##M.insert( \140 {Ctx.Selectors.getUnarySelector(&Ctx.Idents.get(#method)), argument});141#define ADD_METHOD(receiver, method_list, count, argument) \142 receiver##M.insert({Ctx.Selectors.getSelector(count, method_list), argument});143 144/// Initializes a list of methods that require a localized string145/// Format: {"ClassName", {{"selectorName:", LocStringArg#}, ...}, ...}146void NonLocalizedStringChecker::initUIMethods(ASTContext &Ctx) const {147 if (!UIMethods.empty())148 return;149 150 // UI Methods151 NEW_RECEIVER(UISearchDisplayController)152 ADD_UNARY_METHOD(UISearchDisplayController, setSearchResultsTitle, 0)153 154 NEW_RECEIVER(UITabBarItem)155 const IdentifierInfo *initWithTitleUITabBarItemTag[] = {156 &Ctx.Idents.get("initWithTitle"), &Ctx.Idents.get("image"),157 &Ctx.Idents.get("tag")};158 ADD_METHOD(UITabBarItem, initWithTitleUITabBarItemTag, 3, 0)159 const IdentifierInfo *initWithTitleUITabBarItemImage[] = {160 &Ctx.Idents.get("initWithTitle"), &Ctx.Idents.get("image"),161 &Ctx.Idents.get("selectedImage")};162 ADD_METHOD(UITabBarItem, initWithTitleUITabBarItemImage, 3, 0)163 164 NEW_RECEIVER(NSDockTile)165 ADD_UNARY_METHOD(NSDockTile, setBadgeLabel, 0)166 167 NEW_RECEIVER(NSStatusItem)168 ADD_UNARY_METHOD(NSStatusItem, setTitle, 0)169 ADD_UNARY_METHOD(NSStatusItem, setToolTip, 0)170 171 NEW_RECEIVER(UITableViewRowAction)172 const IdentifierInfo *rowActionWithStyleUITableViewRowAction[] = {173 &Ctx.Idents.get("rowActionWithStyle"), &Ctx.Idents.get("title"),174 &Ctx.Idents.get("handler")};175 ADD_METHOD(UITableViewRowAction, rowActionWithStyleUITableViewRowAction, 3, 1)176 ADD_UNARY_METHOD(UITableViewRowAction, setTitle, 0)177 178 NEW_RECEIVER(NSBox)179 ADD_UNARY_METHOD(NSBox, setTitle, 0)180 181 NEW_RECEIVER(NSButton)182 ADD_UNARY_METHOD(NSButton, setTitle, 0)183 ADD_UNARY_METHOD(NSButton, setAlternateTitle, 0)184 const IdentifierInfo *radioButtonWithTitleNSButton[] = {185 &Ctx.Idents.get("radioButtonWithTitle"), &Ctx.Idents.get("target"),186 &Ctx.Idents.get("action")};187 ADD_METHOD(NSButton, radioButtonWithTitleNSButton, 3, 0)188 const IdentifierInfo *buttonWithTitleNSButtonImage[] = {189 &Ctx.Idents.get("buttonWithTitle"), &Ctx.Idents.get("image"),190 &Ctx.Idents.get("target"), &Ctx.Idents.get("action")};191 ADD_METHOD(NSButton, buttonWithTitleNSButtonImage, 4, 0)192 const IdentifierInfo *checkboxWithTitleNSButton[] = {193 &Ctx.Idents.get("checkboxWithTitle"), &Ctx.Idents.get("target"),194 &Ctx.Idents.get("action")};195 ADD_METHOD(NSButton, checkboxWithTitleNSButton, 3, 0)196 const IdentifierInfo *buttonWithTitleNSButtonTarget[] = {197 &Ctx.Idents.get("buttonWithTitle"), &Ctx.Idents.get("target"),198 &Ctx.Idents.get("action")};199 ADD_METHOD(NSButton, buttonWithTitleNSButtonTarget, 3, 0)200 201 NEW_RECEIVER(NSSavePanel)202 ADD_UNARY_METHOD(NSSavePanel, setPrompt, 0)203 ADD_UNARY_METHOD(NSSavePanel, setTitle, 0)204 ADD_UNARY_METHOD(NSSavePanel, setNameFieldLabel, 0)205 ADD_UNARY_METHOD(NSSavePanel, setNameFieldStringValue, 0)206 ADD_UNARY_METHOD(NSSavePanel, setMessage, 0)207 208 NEW_RECEIVER(UIPrintInfo)209 ADD_UNARY_METHOD(UIPrintInfo, setJobName, 0)210 211 NEW_RECEIVER(NSTabViewItem)212 ADD_UNARY_METHOD(NSTabViewItem, setLabel, 0)213 ADD_UNARY_METHOD(NSTabViewItem, setToolTip, 0)214 215 NEW_RECEIVER(NSBrowser)216 const IdentifierInfo *setTitleNSBrowser[] = {&Ctx.Idents.get("setTitle"),217 &Ctx.Idents.get("ofColumn")};218 ADD_METHOD(NSBrowser, setTitleNSBrowser, 2, 0)219 220 NEW_RECEIVER(UIAccessibilityElement)221 ADD_UNARY_METHOD(UIAccessibilityElement, setAccessibilityLabel, 0)222 ADD_UNARY_METHOD(UIAccessibilityElement, setAccessibilityHint, 0)223 ADD_UNARY_METHOD(UIAccessibilityElement, setAccessibilityValue, 0)224 225 NEW_RECEIVER(UIAlertAction)226 const IdentifierInfo *actionWithTitleUIAlertAction[] = {227 &Ctx.Idents.get("actionWithTitle"), &Ctx.Idents.get("style"),228 &Ctx.Idents.get("handler")};229 ADD_METHOD(UIAlertAction, actionWithTitleUIAlertAction, 3, 0)230 231 NEW_RECEIVER(NSPopUpButton)232 ADD_UNARY_METHOD(NSPopUpButton, addItemWithTitle, 0)233 const IdentifierInfo *insertItemWithTitleNSPopUpButton[] = {234 &Ctx.Idents.get("insertItemWithTitle"), &Ctx.Idents.get("atIndex")};235 ADD_METHOD(NSPopUpButton, insertItemWithTitleNSPopUpButton, 2, 0)236 ADD_UNARY_METHOD(NSPopUpButton, removeItemWithTitle, 0)237 ADD_UNARY_METHOD(NSPopUpButton, selectItemWithTitle, 0)238 ADD_UNARY_METHOD(NSPopUpButton, setTitle, 0)239 240 NEW_RECEIVER(NSTableViewRowAction)241 const IdentifierInfo *rowActionWithStyleNSTableViewRowAction[] = {242 &Ctx.Idents.get("rowActionWithStyle"), &Ctx.Idents.get("title"),243 &Ctx.Idents.get("handler")};244 ADD_METHOD(NSTableViewRowAction, rowActionWithStyleNSTableViewRowAction, 3, 1)245 ADD_UNARY_METHOD(NSTableViewRowAction, setTitle, 0)246 247 NEW_RECEIVER(NSImage)248 ADD_UNARY_METHOD(NSImage, setAccessibilityDescription, 0)249 250 NEW_RECEIVER(NSUserActivity)251 ADD_UNARY_METHOD(NSUserActivity, setTitle, 0)252 253 NEW_RECEIVER(NSPathControlItem)254 ADD_UNARY_METHOD(NSPathControlItem, setTitle, 0)255 256 NEW_RECEIVER(NSCell)257 ADD_UNARY_METHOD(NSCell, initTextCell, 0)258 ADD_UNARY_METHOD(NSCell, setTitle, 0)259 ADD_UNARY_METHOD(NSCell, setStringValue, 0)260 261 NEW_RECEIVER(NSPathControl)262 ADD_UNARY_METHOD(NSPathControl, setPlaceholderString, 0)263 264 NEW_RECEIVER(UIAccessibility)265 ADD_UNARY_METHOD(UIAccessibility, setAccessibilityLabel, 0)266 ADD_UNARY_METHOD(UIAccessibility, setAccessibilityHint, 0)267 ADD_UNARY_METHOD(UIAccessibility, setAccessibilityValue, 0)268 269 NEW_RECEIVER(NSTableColumn)270 ADD_UNARY_METHOD(NSTableColumn, setTitle, 0)271 ADD_UNARY_METHOD(NSTableColumn, setHeaderToolTip, 0)272 273 NEW_RECEIVER(NSSegmentedControl)274 const IdentifierInfo *setLabelNSSegmentedControl[] = {275 &Ctx.Idents.get("setLabel"), &Ctx.Idents.get("forSegment")};276 ADD_METHOD(NSSegmentedControl, setLabelNSSegmentedControl, 2, 0)277 const IdentifierInfo *setToolTipNSSegmentedControl[] = {278 &Ctx.Idents.get("setToolTip"), &Ctx.Idents.get("forSegment")};279 ADD_METHOD(NSSegmentedControl, setToolTipNSSegmentedControl, 2, 0)280 281 NEW_RECEIVER(NSButtonCell)282 ADD_UNARY_METHOD(NSButtonCell, setTitle, 0)283 ADD_UNARY_METHOD(NSButtonCell, setAlternateTitle, 0)284 285 NEW_RECEIVER(NSDatePickerCell)286 ADD_UNARY_METHOD(NSDatePickerCell, initTextCell, 0)287 288 NEW_RECEIVER(NSSliderCell)289 ADD_UNARY_METHOD(NSSliderCell, setTitle, 0)290 291 NEW_RECEIVER(NSControl)292 ADD_UNARY_METHOD(NSControl, setStringValue, 0)293 294 NEW_RECEIVER(NSAccessibility)295 ADD_UNARY_METHOD(NSAccessibility, setAccessibilityValueDescription, 0)296 ADD_UNARY_METHOD(NSAccessibility, setAccessibilityLabel, 0)297 ADD_UNARY_METHOD(NSAccessibility, setAccessibilityTitle, 0)298 ADD_UNARY_METHOD(NSAccessibility, setAccessibilityPlaceholderValue, 0)299 ADD_UNARY_METHOD(NSAccessibility, setAccessibilityHelp, 0)300 301 NEW_RECEIVER(NSMatrix)302 const IdentifierInfo *setToolTipNSMatrix[] = {&Ctx.Idents.get("setToolTip"),303 &Ctx.Idents.get("forCell")};304 ADD_METHOD(NSMatrix, setToolTipNSMatrix, 2, 0)305 306 NEW_RECEIVER(NSPrintPanel)307 ADD_UNARY_METHOD(NSPrintPanel, setDefaultButtonTitle, 0)308 309 NEW_RECEIVER(UILocalNotification)310 ADD_UNARY_METHOD(UILocalNotification, setAlertBody, 0)311 ADD_UNARY_METHOD(UILocalNotification, setAlertAction, 0)312 ADD_UNARY_METHOD(UILocalNotification, setAlertTitle, 0)313 314 NEW_RECEIVER(NSSlider)315 ADD_UNARY_METHOD(NSSlider, setTitle, 0)316 317 NEW_RECEIVER(UIMenuItem)318 const IdentifierInfo *initWithTitleUIMenuItem[] = {319 &Ctx.Idents.get("initWithTitle"), &Ctx.Idents.get("action")};320 ADD_METHOD(UIMenuItem, initWithTitleUIMenuItem, 2, 0)321 ADD_UNARY_METHOD(UIMenuItem, setTitle, 0)322 323 NEW_RECEIVER(UIAlertController)324 const IdentifierInfo *alertControllerWithTitleUIAlertController[] = {325 &Ctx.Idents.get("alertControllerWithTitle"), &Ctx.Idents.get("message"),326 &Ctx.Idents.get("preferredStyle")};327 ADD_METHOD(UIAlertController, alertControllerWithTitleUIAlertController, 3, 1)328 ADD_UNARY_METHOD(UIAlertController, setTitle, 0)329 ADD_UNARY_METHOD(UIAlertController, setMessage, 0)330 331 NEW_RECEIVER(UIApplicationShortcutItem)332 const IdentifierInfo *initWithTypeUIApplicationShortcutItemIcon[] = {333 &Ctx.Idents.get("initWithType"), &Ctx.Idents.get("localizedTitle"),334 &Ctx.Idents.get("localizedSubtitle"), &Ctx.Idents.get("icon"),335 &Ctx.Idents.get("userInfo")};336 ADD_METHOD(UIApplicationShortcutItem,337 initWithTypeUIApplicationShortcutItemIcon, 5, 1)338 const IdentifierInfo *initWithTypeUIApplicationShortcutItem[] = {339 &Ctx.Idents.get("initWithType"), &Ctx.Idents.get("localizedTitle")};340 ADD_METHOD(UIApplicationShortcutItem, initWithTypeUIApplicationShortcutItem,341 2, 1)342 343 NEW_RECEIVER(UIActionSheet)344 const IdentifierInfo *initWithTitleUIActionSheet[] = {345 &Ctx.Idents.get("initWithTitle"), &Ctx.Idents.get("delegate"),346 &Ctx.Idents.get("cancelButtonTitle"),347 &Ctx.Idents.get("destructiveButtonTitle"),348 &Ctx.Idents.get("otherButtonTitles")};349 ADD_METHOD(UIActionSheet, initWithTitleUIActionSheet, 5, 0)350 ADD_UNARY_METHOD(UIActionSheet, addButtonWithTitle, 0)351 ADD_UNARY_METHOD(UIActionSheet, setTitle, 0)352 353 NEW_RECEIVER(UIAccessibilityCustomAction)354 const IdentifierInfo *initWithNameUIAccessibilityCustomAction[] = {355 &Ctx.Idents.get("initWithName"), &Ctx.Idents.get("target"),356 &Ctx.Idents.get("selector")};357 ADD_METHOD(UIAccessibilityCustomAction,358 initWithNameUIAccessibilityCustomAction, 3, 0)359 ADD_UNARY_METHOD(UIAccessibilityCustomAction, setName, 0)360 361 NEW_RECEIVER(UISearchBar)362 ADD_UNARY_METHOD(UISearchBar, setText, 0)363 ADD_UNARY_METHOD(UISearchBar, setPrompt, 0)364 ADD_UNARY_METHOD(UISearchBar, setPlaceholder, 0)365 366 NEW_RECEIVER(UIBarItem)367 ADD_UNARY_METHOD(UIBarItem, setTitle, 0)368 369 NEW_RECEIVER(UITextView)370 ADD_UNARY_METHOD(UITextView, setText, 0)371 372 NEW_RECEIVER(NSView)373 ADD_UNARY_METHOD(NSView, setToolTip, 0)374 375 NEW_RECEIVER(NSTextField)376 ADD_UNARY_METHOD(NSTextField, setPlaceholderString, 0)377 ADD_UNARY_METHOD(NSTextField, textFieldWithString, 0)378 ADD_UNARY_METHOD(NSTextField, wrappingLabelWithString, 0)379 ADD_UNARY_METHOD(NSTextField, labelWithString, 0)380 381 NEW_RECEIVER(NSAttributedString)382 ADD_UNARY_METHOD(NSAttributedString, initWithString, 0)383 const IdentifierInfo *initWithStringNSAttributedString[] = {384 &Ctx.Idents.get("initWithString"), &Ctx.Idents.get("attributes")};385 ADD_METHOD(NSAttributedString, initWithStringNSAttributedString, 2, 0)386 387 NEW_RECEIVER(NSText)388 ADD_UNARY_METHOD(NSText, setString, 0)389 390 NEW_RECEIVER(UIKeyCommand)391 const IdentifierInfo *keyCommandWithInputUIKeyCommand[] = {392 &Ctx.Idents.get("keyCommandWithInput"), &Ctx.Idents.get("modifierFlags"),393 &Ctx.Idents.get("action"), &Ctx.Idents.get("discoverabilityTitle")};394 ADD_METHOD(UIKeyCommand, keyCommandWithInputUIKeyCommand, 4, 3)395 ADD_UNARY_METHOD(UIKeyCommand, setDiscoverabilityTitle, 0)396 397 NEW_RECEIVER(UILabel)398 ADD_UNARY_METHOD(UILabel, setText, 0)399 400 NEW_RECEIVER(NSAlert)401 const IdentifierInfo *alertWithMessageTextNSAlert[] = {402 &Ctx.Idents.get("alertWithMessageText"), &Ctx.Idents.get("defaultButton"),403 &Ctx.Idents.get("alternateButton"), &Ctx.Idents.get("otherButton"),404 &Ctx.Idents.get("informativeTextWithFormat")};405 ADD_METHOD(NSAlert, alertWithMessageTextNSAlert, 5, 0)406 ADD_UNARY_METHOD(NSAlert, addButtonWithTitle, 0)407 ADD_UNARY_METHOD(NSAlert, setMessageText, 0)408 ADD_UNARY_METHOD(NSAlert, setInformativeText, 0)409 ADD_UNARY_METHOD(NSAlert, setHelpAnchor, 0)410 411 NEW_RECEIVER(UIMutableApplicationShortcutItem)412 ADD_UNARY_METHOD(UIMutableApplicationShortcutItem, setLocalizedTitle, 0)413 ADD_UNARY_METHOD(UIMutableApplicationShortcutItem, setLocalizedSubtitle, 0)414 415 NEW_RECEIVER(UIButton)416 const IdentifierInfo *setTitleUIButton[] = {&Ctx.Idents.get("setTitle"),417 &Ctx.Idents.get("forState")};418 ADD_METHOD(UIButton, setTitleUIButton, 2, 0)419 420 NEW_RECEIVER(NSWindow)421 ADD_UNARY_METHOD(NSWindow, setTitle, 0)422 const IdentifierInfo *minFrameWidthWithTitleNSWindow[] = {423 &Ctx.Idents.get("minFrameWidthWithTitle"), &Ctx.Idents.get("styleMask")};424 ADD_METHOD(NSWindow, minFrameWidthWithTitleNSWindow, 2, 0)425 ADD_UNARY_METHOD(NSWindow, setMiniwindowTitle, 0)426 427 NEW_RECEIVER(NSPathCell)428 ADD_UNARY_METHOD(NSPathCell, setPlaceholderString, 0)429 430 NEW_RECEIVER(UIDocumentMenuViewController)431 const IdentifierInfo *addOptionWithTitleUIDocumentMenuViewController[] = {432 &Ctx.Idents.get("addOptionWithTitle"), &Ctx.Idents.get("image"),433 &Ctx.Idents.get("order"), &Ctx.Idents.get("handler")};434 ADD_METHOD(UIDocumentMenuViewController,435 addOptionWithTitleUIDocumentMenuViewController, 4, 0)436 437 NEW_RECEIVER(UINavigationItem)438 ADD_UNARY_METHOD(UINavigationItem, initWithTitle, 0)439 ADD_UNARY_METHOD(UINavigationItem, setTitle, 0)440 ADD_UNARY_METHOD(UINavigationItem, setPrompt, 0)441 442 NEW_RECEIVER(UIAlertView)443 const IdentifierInfo *initWithTitleUIAlertView[] = {444 &Ctx.Idents.get("initWithTitle"), &Ctx.Idents.get("message"),445 &Ctx.Idents.get("delegate"), &Ctx.Idents.get("cancelButtonTitle"),446 &Ctx.Idents.get("otherButtonTitles")};447 ADD_METHOD(UIAlertView, initWithTitleUIAlertView, 5, 0)448 ADD_UNARY_METHOD(UIAlertView, addButtonWithTitle, 0)449 ADD_UNARY_METHOD(UIAlertView, setTitle, 0)450 ADD_UNARY_METHOD(UIAlertView, setMessage, 0)451 452 NEW_RECEIVER(NSFormCell)453 ADD_UNARY_METHOD(NSFormCell, initTextCell, 0)454 ADD_UNARY_METHOD(NSFormCell, setTitle, 0)455 ADD_UNARY_METHOD(NSFormCell, setPlaceholderString, 0)456 457 NEW_RECEIVER(NSUserNotification)458 ADD_UNARY_METHOD(NSUserNotification, setTitle, 0)459 ADD_UNARY_METHOD(NSUserNotification, setSubtitle, 0)460 ADD_UNARY_METHOD(NSUserNotification, setInformativeText, 0)461 ADD_UNARY_METHOD(NSUserNotification, setActionButtonTitle, 0)462 ADD_UNARY_METHOD(NSUserNotification, setOtherButtonTitle, 0)463 ADD_UNARY_METHOD(NSUserNotification, setResponsePlaceholder, 0)464 465 NEW_RECEIVER(NSToolbarItem)466 ADD_UNARY_METHOD(NSToolbarItem, setLabel, 0)467 ADD_UNARY_METHOD(NSToolbarItem, setPaletteLabel, 0)468 ADD_UNARY_METHOD(NSToolbarItem, setToolTip, 0)469 470 NEW_RECEIVER(NSProgress)471 ADD_UNARY_METHOD(NSProgress, setLocalizedDescription, 0)472 ADD_UNARY_METHOD(NSProgress, setLocalizedAdditionalDescription, 0)473 474 NEW_RECEIVER(NSSegmentedCell)475 const IdentifierInfo *setLabelNSSegmentedCell[] = {476 &Ctx.Idents.get("setLabel"), &Ctx.Idents.get("forSegment")};477 ADD_METHOD(NSSegmentedCell, setLabelNSSegmentedCell, 2, 0)478 const IdentifierInfo *setToolTipNSSegmentedCell[] = {479 &Ctx.Idents.get("setToolTip"), &Ctx.Idents.get("forSegment")};480 ADD_METHOD(NSSegmentedCell, setToolTipNSSegmentedCell, 2, 0)481 482 NEW_RECEIVER(NSUndoManager)483 ADD_UNARY_METHOD(NSUndoManager, setActionName, 0)484 ADD_UNARY_METHOD(NSUndoManager, undoMenuTitleForUndoActionName, 0)485 ADD_UNARY_METHOD(NSUndoManager, redoMenuTitleForUndoActionName, 0)486 487 NEW_RECEIVER(NSMenuItem)488 const IdentifierInfo *initWithTitleNSMenuItem[] = {489 &Ctx.Idents.get("initWithTitle"), &Ctx.Idents.get("action"),490 &Ctx.Idents.get("keyEquivalent")};491 ADD_METHOD(NSMenuItem, initWithTitleNSMenuItem, 3, 0)492 ADD_UNARY_METHOD(NSMenuItem, setTitle, 0)493 ADD_UNARY_METHOD(NSMenuItem, setToolTip, 0)494 495 NEW_RECEIVER(NSPopUpButtonCell)496 const IdentifierInfo *initTextCellNSPopUpButtonCell[] = {497 &Ctx.Idents.get("initTextCell"), &Ctx.Idents.get("pullsDown")};498 ADD_METHOD(NSPopUpButtonCell, initTextCellNSPopUpButtonCell, 2, 0)499 ADD_UNARY_METHOD(NSPopUpButtonCell, addItemWithTitle, 0)500 const IdentifierInfo *insertItemWithTitleNSPopUpButtonCell[] = {501 &Ctx.Idents.get("insertItemWithTitle"), &Ctx.Idents.get("atIndex")};502 ADD_METHOD(NSPopUpButtonCell, insertItemWithTitleNSPopUpButtonCell, 2, 0)503 ADD_UNARY_METHOD(NSPopUpButtonCell, removeItemWithTitle, 0)504 ADD_UNARY_METHOD(NSPopUpButtonCell, selectItemWithTitle, 0)505 ADD_UNARY_METHOD(NSPopUpButtonCell, setTitle, 0)506 507 NEW_RECEIVER(NSViewController)508 ADD_UNARY_METHOD(NSViewController, setTitle, 0)509 510 NEW_RECEIVER(NSMenu)511 ADD_UNARY_METHOD(NSMenu, initWithTitle, 0)512 const IdentifierInfo *insertItemWithTitleNSMenu[] = {513 &Ctx.Idents.get("insertItemWithTitle"), &Ctx.Idents.get("action"),514 &Ctx.Idents.get("keyEquivalent"), &Ctx.Idents.get("atIndex")};515 ADD_METHOD(NSMenu, insertItemWithTitleNSMenu, 4, 0)516 const IdentifierInfo *addItemWithTitleNSMenu[] = {517 &Ctx.Idents.get("addItemWithTitle"), &Ctx.Idents.get("action"),518 &Ctx.Idents.get("keyEquivalent")};519 ADD_METHOD(NSMenu, addItemWithTitleNSMenu, 3, 0)520 ADD_UNARY_METHOD(NSMenu, setTitle, 0)521 522 NEW_RECEIVER(UIMutableUserNotificationAction)523 ADD_UNARY_METHOD(UIMutableUserNotificationAction, setTitle, 0)524 525 NEW_RECEIVER(NSForm)526 ADD_UNARY_METHOD(NSForm, addEntry, 0)527 const IdentifierInfo *insertEntryNSForm[] = {&Ctx.Idents.get("insertEntry"),528 &Ctx.Idents.get("atIndex")};529 ADD_METHOD(NSForm, insertEntryNSForm, 2, 0)530 531 NEW_RECEIVER(NSTextFieldCell)532 ADD_UNARY_METHOD(NSTextFieldCell, setPlaceholderString, 0)533 534 NEW_RECEIVER(NSUserNotificationAction)535 const IdentifierInfo *actionWithIdentifierNSUserNotificationAction[] = {536 &Ctx.Idents.get("actionWithIdentifier"), &Ctx.Idents.get("title")};537 ADD_METHOD(NSUserNotificationAction,538 actionWithIdentifierNSUserNotificationAction, 2, 1)539 540 NEW_RECEIVER(UITextField)541 ADD_UNARY_METHOD(UITextField, setText, 0)542 ADD_UNARY_METHOD(UITextField, setPlaceholder, 0)543 544 NEW_RECEIVER(UIBarButtonItem)545 const IdentifierInfo *initWithTitleUIBarButtonItem[] = {546 &Ctx.Idents.get("initWithTitle"), &Ctx.Idents.get("style"),547 &Ctx.Idents.get("target"), &Ctx.Idents.get("action")};548 ADD_METHOD(UIBarButtonItem, initWithTitleUIBarButtonItem, 4, 0)549 550 NEW_RECEIVER(UIViewController)551 ADD_UNARY_METHOD(UIViewController, setTitle, 0)552 553 NEW_RECEIVER(UISegmentedControl)554 const IdentifierInfo *insertSegmentWithTitleUISegmentedControl[] = {555 &Ctx.Idents.get("insertSegmentWithTitle"), &Ctx.Idents.get("atIndex"),556 &Ctx.Idents.get("animated")};557 ADD_METHOD(UISegmentedControl, insertSegmentWithTitleUISegmentedControl, 3, 0)558 const IdentifierInfo *setTitleUISegmentedControl[] = {559 &Ctx.Idents.get("setTitle"), &Ctx.Idents.get("forSegmentAtIndex")};560 ADD_METHOD(UISegmentedControl, setTitleUISegmentedControl, 2, 0)561 562 NEW_RECEIVER(NSAccessibilityCustomRotorItemResult)563 const IdentifierInfo564 *initWithItemLoadingTokenNSAccessibilityCustomRotorItemResult[] = {565 &Ctx.Idents.get("initWithItemLoadingToken"),566 &Ctx.Idents.get("customLabel")};567 ADD_METHOD(NSAccessibilityCustomRotorItemResult,568 initWithItemLoadingTokenNSAccessibilityCustomRotorItemResult, 2, 1)569 ADD_UNARY_METHOD(NSAccessibilityCustomRotorItemResult, setCustomLabel, 0)570 571 NEW_RECEIVER(UIContextualAction)572 const IdentifierInfo *contextualActionWithStyleUIContextualAction[] = {573 &Ctx.Idents.get("contextualActionWithStyle"), &Ctx.Idents.get("title"),574 &Ctx.Idents.get("handler")};575 ADD_METHOD(UIContextualAction, contextualActionWithStyleUIContextualAction, 3,576 1)577 ADD_UNARY_METHOD(UIContextualAction, setTitle, 0)578 579 NEW_RECEIVER(NSAccessibilityCustomRotor)580 const IdentifierInfo *initWithLabelNSAccessibilityCustomRotor[] = {581 &Ctx.Idents.get("initWithLabel"), &Ctx.Idents.get("itemSearchDelegate")};582 ADD_METHOD(NSAccessibilityCustomRotor,583 initWithLabelNSAccessibilityCustomRotor, 2, 0)584 ADD_UNARY_METHOD(NSAccessibilityCustomRotor, setLabel, 0)585 586 NEW_RECEIVER(NSWindowTab)587 ADD_UNARY_METHOD(NSWindowTab, setTitle, 0)588 ADD_UNARY_METHOD(NSWindowTab, setToolTip, 0)589 590 NEW_RECEIVER(NSAccessibilityCustomAction)591 const IdentifierInfo *initWithNameNSAccessibilityCustomAction[] = {592 &Ctx.Idents.get("initWithName"), &Ctx.Idents.get("handler")};593 ADD_METHOD(NSAccessibilityCustomAction,594 initWithNameNSAccessibilityCustomAction, 2, 0)595 const IdentifierInfo *initWithNameTargetNSAccessibilityCustomAction[] = {596 &Ctx.Idents.get("initWithName"), &Ctx.Idents.get("target"),597 &Ctx.Idents.get("selector")};598 ADD_METHOD(NSAccessibilityCustomAction,599 initWithNameTargetNSAccessibilityCustomAction, 3, 0)600 ADD_UNARY_METHOD(NSAccessibilityCustomAction, setName, 0)601}602 603#define LSF_INSERT(function_name) LSF.insert(&Ctx.Idents.get(function_name));604#define LSM_INSERT_NULLARY(receiver, method_name) \605 LSM.insert({&Ctx.Idents.get(receiver), Ctx.Selectors.getNullarySelector( \606 &Ctx.Idents.get(method_name))});607#define LSM_INSERT_UNARY(receiver, method_name) \608 LSM.insert({&Ctx.Idents.get(receiver), \609 Ctx.Selectors.getUnarySelector(&Ctx.Idents.get(method_name))});610#define LSM_INSERT_SELECTOR(receiver, method_list, arguments) \611 LSM.insert({&Ctx.Idents.get(receiver), \612 Ctx.Selectors.getSelector(arguments, method_list)});613 614/// Initializes a list of methods and C functions that return a localized string615void NonLocalizedStringChecker::initLocStringsMethods(ASTContext &Ctx) const {616 if (!LSM.empty())617 return;618 619 const IdentifierInfo *LocalizedStringMacro[] = {620 &Ctx.Idents.get("localizedStringForKey"), &Ctx.Idents.get("value"),621 &Ctx.Idents.get("table")};622 LSM_INSERT_SELECTOR("NSBundle", LocalizedStringMacro, 3)623 LSM_INSERT_UNARY("NSDateFormatter", "stringFromDate")624 const IdentifierInfo *LocalizedStringFromDate[] = {625 &Ctx.Idents.get("localizedStringFromDate"), &Ctx.Idents.get("dateStyle"),626 &Ctx.Idents.get("timeStyle")};627 LSM_INSERT_SELECTOR("NSDateFormatter", LocalizedStringFromDate, 3)628 LSM_INSERT_UNARY("NSNumberFormatter", "stringFromNumber")629 LSM_INSERT_NULLARY("UITextField", "text")630 LSM_INSERT_NULLARY("UITextView", "text")631 LSM_INSERT_NULLARY("UILabel", "text")632 633 LSF_INSERT("CFDateFormatterCreateStringWithDate");634 LSF_INSERT("CFDateFormatterCreateStringWithAbsoluteTime");635 LSF_INSERT("CFNumberFormatterCreateStringWithNumber");636}637 638/// Checks to see if the method / function declaration includes639/// __attribute__((annotate("returns_localized_nsstring")))640bool NonLocalizedStringChecker::isAnnotatedAsReturningLocalized(641 const Decl *D) const {642 if (!D)643 return false;644 return std::any_of(645 D->specific_attr_begin<AnnotateAttr>(),646 D->specific_attr_end<AnnotateAttr>(), [](const AnnotateAttr *Ann) {647 return Ann->getAnnotation() == "returns_localized_nsstring";648 });649}650 651/// Checks to see if the method / function declaration includes652/// __attribute__((annotate("takes_localized_nsstring")))653bool NonLocalizedStringChecker::isAnnotatedAsTakingLocalized(654 const Decl *D) const {655 if (!D)656 return false;657 return std::any_of(658 D->specific_attr_begin<AnnotateAttr>(),659 D->specific_attr_end<AnnotateAttr>(), [](const AnnotateAttr *Ann) {660 return Ann->getAnnotation() == "takes_localized_nsstring";661 });662}663 664/// Returns true if the given SVal is marked as Localized in the program state665bool NonLocalizedStringChecker::hasLocalizedState(SVal S,666 CheckerContext &C) const {667 const MemRegion *mt = S.getAsRegion();668 if (mt) {669 const LocalizedState *LS = C.getState()->get<LocalizedMemMap>(mt);670 if (LS && LS->isLocalized())671 return true;672 }673 return false;674}675 676/// Returns true if the given SVal is marked as NonLocalized in the program677/// state678bool NonLocalizedStringChecker::hasNonLocalizedState(SVal S,679 CheckerContext &C) const {680 const MemRegion *mt = S.getAsRegion();681 if (mt) {682 const LocalizedState *LS = C.getState()->get<LocalizedMemMap>(mt);683 if (LS && LS->isNonLocalized())684 return true;685 }686 return false;687}688 689/// Marks the given SVal as Localized in the program state690void NonLocalizedStringChecker::setLocalizedState(const SVal S,691 CheckerContext &C) const {692 const MemRegion *mt = S.getAsRegion();693 if (mt) {694 ProgramStateRef State =695 C.getState()->set<LocalizedMemMap>(mt, LocalizedState::getLocalized());696 C.addTransition(State);697 }698}699 700/// Marks the given SVal as NonLocalized in the program state701void NonLocalizedStringChecker::setNonLocalizedState(const SVal S,702 CheckerContext &C) const {703 const MemRegion *mt = S.getAsRegion();704 if (mt) {705 ProgramStateRef State = C.getState()->set<LocalizedMemMap>(706 mt, LocalizedState::getNonLocalized());707 C.addTransition(State);708 }709}710 711 712static bool isDebuggingName(std::string name) {713 return StringRef(name).contains_insensitive("debug");714}715 716/// Returns true when, heuristically, the analyzer may be analyzing debugging717/// code. We use this to suppress localization diagnostics in un-localized user718/// interfaces that are only used for debugging and are therefore not user719/// facing.720static bool isDebuggingContext(CheckerContext &C) {721 const Decl *D = C.getCurrentAnalysisDeclContext()->getDecl();722 if (!D)723 return false;724 725 if (auto *ND = dyn_cast<NamedDecl>(D)) {726 if (isDebuggingName(ND->getNameAsString()))727 return true;728 }729 730 const DeclContext *DC = D->getDeclContext();731 732 if (auto *CD = dyn_cast<ObjCContainerDecl>(DC)) {733 if (isDebuggingName(CD->getNameAsString()))734 return true;735 }736 737 return false;738}739 740 741/// Reports a localization error for the passed in method call and SVal742void NonLocalizedStringChecker::reportLocalizationError(743 SVal S, const CallEvent &M, CheckerContext &C, int argumentNumber) const {744 745 // Don't warn about localization errors in classes and methods that746 // may be debug code.747 if (isDebuggingContext(C))748 return;749 750 ExplodedNode *ErrNode = C.generateNonFatalErrorNode();751 752 if (!ErrNode)753 return;754 755 // Generate the bug report.756 auto R = std::make_unique<PathSensitiveBugReport>(757 BT, "User-facing text should use localized string macro", ErrNode);758 if (argumentNumber) {759 R->addRange(M.getArgExpr(argumentNumber - 1)->getSourceRange());760 } else {761 R->addRange(M.getSourceRange());762 }763 R->markInteresting(S);764 765 const MemRegion *StringRegion = S.getAsRegion();766 if (StringRegion)767 R->addVisitor(std::make_unique<NonLocalizedStringBRVisitor>(StringRegion));768 769 C.emitReport(std::move(R));770}771 772/// Returns the argument number requiring localized string if it exists773/// otherwise, returns -1774int NonLocalizedStringChecker::getLocalizedArgumentForSelector(775 const IdentifierInfo *Receiver, Selector S) const {776 auto method = UIMethods.find(Receiver);777 778 if (method == UIMethods.end())779 return -1;780 781 auto argumentIterator = method->getSecond().find(S);782 783 if (argumentIterator == method->getSecond().end())784 return -1;785 786 int argumentNumber = argumentIterator->getSecond();787 return argumentNumber;788}789 790/// Check if the string being passed in has NonLocalized state791void NonLocalizedStringChecker::checkPreObjCMessage(const ObjCMethodCall &msg,792 CheckerContext &C) const {793 initUIMethods(C.getASTContext());794 795 const ObjCInterfaceDecl *OD = msg.getReceiverInterface();796 if (!OD)797 return;798 const IdentifierInfo *odInfo = OD->getIdentifier();799 800 Selector S = msg.getSelector();801 802 std::string SelectorString = S.getAsString();803 StringRef SelectorName = SelectorString;804 assert(!SelectorName.empty());805 806 if (odInfo->isStr("NSString")) {807 // Handle the case where the receiver is an NSString808 // These special NSString methods draw to the screen809 810 if (!(SelectorName.starts_with("drawAtPoint") ||811 SelectorName.starts_with("drawInRect") ||812 SelectorName.starts_with("drawWithRect")))813 return;814 815 SVal svTitle = msg.getReceiverSVal();816 817 bool isNonLocalized = hasNonLocalizedState(svTitle, C);818 819 if (isNonLocalized) {820 reportLocalizationError(svTitle, msg, C);821 }822 }823 824 int argumentNumber = getLocalizedArgumentForSelector(odInfo, S);825 // Go up each hierarchy of superclasses and their protocols826 while (argumentNumber < 0 && OD->getSuperClass() != nullptr) {827 for (const auto *P : OD->all_referenced_protocols()) {828 argumentNumber = getLocalizedArgumentForSelector(P->getIdentifier(), S);829 if (argumentNumber >= 0)830 break;831 }832 if (argumentNumber < 0) {833 OD = OD->getSuperClass();834 argumentNumber = getLocalizedArgumentForSelector(OD->getIdentifier(), S);835 }836 }837 838 if (argumentNumber < 0) { // There was no match in UIMethods839 if (const Decl *D = msg.getDecl()) {840 if (const ObjCMethodDecl *OMD = dyn_cast_or_null<ObjCMethodDecl>(D)) {841 for (auto [Idx, FormalParam] : llvm::enumerate(OMD->parameters())) {842 if (isAnnotatedAsTakingLocalized(FormalParam)) {843 argumentNumber = Idx;844 break;845 }846 }847 }848 }849 }850 851 if (argumentNumber < 0) // Still no match852 return;853 854 SVal svTitle = msg.getArgSVal(argumentNumber);855 856 if (const ObjCStringRegion *SR =857 dyn_cast_or_null<ObjCStringRegion>(svTitle.getAsRegion())) {858 StringRef stringValue =859 SR->getObjCStringLiteral()->getString()->getString();860 if ((stringValue.trim().size() == 0 && stringValue.size() > 0) ||861 stringValue.empty())862 return;863 if (!IsAggressive && llvm::sys::unicode::columnWidthUTF8(stringValue) < 2)864 return;865 }866 867 bool isNonLocalized = hasNonLocalizedState(svTitle, C);868 869 if (isNonLocalized) {870 reportLocalizationError(svTitle, msg, C, argumentNumber + 1);871 }872}873 874void NonLocalizedStringChecker::checkPreCall(const CallEvent &Call,875 CheckerContext &C) const {876 const auto *FD = dyn_cast_or_null<FunctionDecl>(Call.getDecl());877 if (!FD)878 return;879 880 auto formals = FD->parameters();881 for (unsigned i = 0, ei = std::min(static_cast<unsigned>(formals.size()),882 Call.getNumArgs()); i != ei; ++i) {883 if (isAnnotatedAsTakingLocalized(formals[i])) {884 auto actual = Call.getArgSVal(i);885 if (hasNonLocalizedState(actual, C)) {886 reportLocalizationError(actual, Call, C, i + 1);887 }888 }889 }890}891 892static inline bool isNSStringType(QualType T, ASTContext &Ctx) {893 894 const ObjCObjectPointerType *PT = T->getAs<ObjCObjectPointerType>();895 if (!PT)896 return false;897 898 ObjCInterfaceDecl *Cls = PT->getObjectType()->getInterface();899 if (!Cls)900 return false;901 902 const IdentifierInfo *ClsName = Cls->getIdentifier();903 904 // FIXME: Should we walk the chain of classes?905 return ClsName == &Ctx.Idents.get("NSString") ||906 ClsName == &Ctx.Idents.get("NSMutableString");907}908 909/// Marks a string being returned by any call as localized910/// if it is in LocStringFunctions (LSF) or the function is annotated.911/// Otherwise, we mark it as NonLocalized (Aggressive) or912/// NonLocalized only if it is not backed by a SymRegion (Non-Aggressive),913/// basically leaving only string literals as NonLocalized.914void NonLocalizedStringChecker::checkPostCall(const CallEvent &Call,915 CheckerContext &C) const {916 initLocStringsMethods(C.getASTContext());917 918 if (!Call.getOriginExpr())919 return;920 921 // Anything that takes in a localized NSString as an argument922 // and returns an NSString will be assumed to be returning a923 // localized NSString. (Counter: Incorrectly combining two LocalizedStrings)924 const QualType RT = Call.getResultType();925 if (isNSStringType(RT, C.getASTContext())) {926 for (unsigned i = 0; i < Call.getNumArgs(); ++i) {927 SVal argValue = Call.getArgSVal(i);928 if (hasLocalizedState(argValue, C)) {929 SVal sv = Call.getReturnValue();930 setLocalizedState(sv, C);931 return;932 }933 }934 }935 936 const Decl *D = Call.getDecl();937 if (!D)938 return;939 940 const IdentifierInfo *Identifier = Call.getCalleeIdentifier();941 942 SVal sv = Call.getReturnValue();943 if (isAnnotatedAsReturningLocalized(D) || LSF.contains(Identifier)) {944 setLocalizedState(sv, C);945 } else if (isNSStringType(RT, C.getASTContext()) &&946 !hasLocalizedState(sv, C)) {947 if (IsAggressive) {948 setNonLocalizedState(sv, C);949 } else {950 const SymbolicRegion *SymReg =951 dyn_cast_or_null<SymbolicRegion>(sv.getAsRegion());952 if (!SymReg)953 setNonLocalizedState(sv, C);954 }955 }956}957 958/// Marks a string being returned by an ObjC method as localized959/// if it is in LocStringMethods or the method is annotated960void NonLocalizedStringChecker::checkPostObjCMessage(const ObjCMethodCall &msg,961 CheckerContext &C) const {962 initLocStringsMethods(C.getASTContext());963 964 if (!msg.isInstanceMessage())965 return;966 967 const ObjCInterfaceDecl *OD = msg.getReceiverInterface();968 if (!OD)969 return;970 const IdentifierInfo *odInfo = OD->getIdentifier();971 972 Selector S = msg.getSelector();973 974 std::pair<const IdentifierInfo *, Selector> MethodDescription = {odInfo, S};975 976 if (LSM.count(MethodDescription) ||977 isAnnotatedAsReturningLocalized(msg.getDecl())) {978 SVal sv = msg.getReturnValue();979 setLocalizedState(sv, C);980 }981}982 983/// Marks all empty string literals as localized984void NonLocalizedStringChecker::checkPostStmt(const ObjCStringLiteral *SL,985 CheckerContext &C) const {986 SVal sv = C.getSVal(SL);987 setNonLocalizedState(sv, C);988}989 990PathDiagnosticPieceRef991NonLocalizedStringBRVisitor::VisitNode(const ExplodedNode *Succ,992 BugReporterContext &BRC,993 PathSensitiveBugReport &BR) {994 if (Satisfied)995 return nullptr;996 997 std::optional<StmtPoint> Point = Succ->getLocation().getAs<StmtPoint>();998 if (!Point)999 return nullptr;1000 1001 auto *LiteralExpr = dyn_cast<ObjCStringLiteral>(Point->getStmt());1002 if (!LiteralExpr)1003 return nullptr;1004 1005 SVal LiteralSVal = Succ->getSVal(LiteralExpr);1006 if (LiteralSVal.getAsRegion() != NonLocalizedString)1007 return nullptr;1008 1009 Satisfied = true;1010 1011 PathDiagnosticLocation L =1012 PathDiagnosticLocation::create(*Point, BRC.getSourceManager());1013 1014 if (!L.isValid() || !L.asLocation().isValid())1015 return nullptr;1016 1017 auto Piece = std::make_shared<PathDiagnosticEventPiece>(1018 L, "Non-localized string literal here");1019 Piece->addRange(LiteralExpr->getSourceRange());1020 1021 return std::move(Piece);1022}1023 1024namespace {1025class EmptyLocalizationContextChecker1026 : public Checker<check::ASTDecl<ObjCImplementationDecl>> {1027 1028 // A helper class, which walks the AST1029 class MethodCrawler : public ConstStmtVisitor<MethodCrawler> {1030 const ObjCMethodDecl *MD;1031 BugReporter &BR;1032 AnalysisManager &Mgr;1033 const CheckerBase *Checker;1034 LocationOrAnalysisDeclContext DCtx;1035 1036 public:1037 MethodCrawler(const ObjCMethodDecl *InMD, BugReporter &InBR,1038 const CheckerBase *Checker, AnalysisManager &InMgr,1039 AnalysisDeclContext *InDCtx)1040 : MD(InMD), BR(InBR), Mgr(InMgr), Checker(Checker), DCtx(InDCtx) {}1041 1042 void VisitStmt(const Stmt *S) { VisitChildren(S); }1043 1044 void VisitObjCMessageExpr(const ObjCMessageExpr *ME);1045 1046 void reportEmptyContextError(const ObjCMessageExpr *M) const;1047 1048 void VisitChildren(const Stmt *S) {1049 for (const Stmt *Child : S->children()) {1050 if (Child)1051 this->Visit(Child);1052 }1053 }1054 };1055 1056public:1057 void checkASTDecl(const ObjCImplementationDecl *D, AnalysisManager &Mgr,1058 BugReporter &BR) const;1059};1060} // end anonymous namespace1061 1062void EmptyLocalizationContextChecker::checkASTDecl(1063 const ObjCImplementationDecl *D, AnalysisManager &Mgr,1064 BugReporter &BR) const {1065 1066 for (const ObjCMethodDecl *M : D->methods()) {1067 AnalysisDeclContext *DCtx = Mgr.getAnalysisDeclContext(M);1068 1069 const Stmt *Body = M->getBody();1070 if (!Body) {1071 assert(M->isSynthesizedAccessorStub());1072 continue;1073 }1074 1075 MethodCrawler MC(M->getCanonicalDecl(), BR, this, Mgr, DCtx);1076 MC.VisitStmt(Body);1077 }1078}1079 1080/// This check attempts to match these macros, assuming they are defined as1081/// follows:1082///1083/// #define NSLocalizedString(key, comment) \1084/// [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]1085/// #define NSLocalizedStringFromTable(key, tbl, comment) \1086/// [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)]1087/// #define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \1088/// [bundle localizedStringForKey:(key) value:@"" table:(tbl)]1089/// #define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)1090///1091/// We cannot use the path sensitive check because the macro argument we are1092/// checking for (comment) is not used and thus not present in the AST,1093/// so we use Lexer on the original macro call and retrieve the value of1094/// the comment. If it's empty or nil, we raise a warning.1095void EmptyLocalizationContextChecker::MethodCrawler::VisitObjCMessageExpr(1096 const ObjCMessageExpr *ME) {1097 1098 // FIXME: We may be able to use PPCallbacks to check for empty context1099 // comments as part of preprocessing and avoid this re-lexing hack.1100 const ObjCInterfaceDecl *OD = ME->getReceiverInterface();1101 if (!OD)1102 return;1103 1104 const IdentifierInfo *odInfo = OD->getIdentifier();1105 1106 if (!(odInfo->isStr("NSBundle") &&1107 ME->getSelector().getAsString() ==1108 "localizedStringForKey:value:table:")) {1109 return;1110 }1111 1112 SourceRange R = ME->getSourceRange();1113 if (!R.getBegin().isMacroID())1114 return;1115 1116 // getImmediateMacroCallerLoc gets the location of the immediate macro1117 // caller, one level up the stack toward the initial macro typed into the1118 // source, so SL should point to the NSLocalizedString macro.1119 SourceLocation SL =1120 Mgr.getSourceManager().getImmediateMacroCallerLoc(R.getBegin());1121 FileIDAndOffset SLInfo = Mgr.getSourceManager().getDecomposedLoc(SL);1122 1123 SrcMgr::SLocEntry SE = Mgr.getSourceManager().getSLocEntry(SLInfo.first);1124 1125 // If NSLocalizedString macro is wrapped in another macro, we need to1126 // unwrap the expansion until we get to the NSLocalizedStringMacro.1127 while (SE.isExpansion()) {1128 SL = SE.getExpansion().getSpellingLoc();1129 SLInfo = Mgr.getSourceManager().getDecomposedLoc(SL);1130 SE = Mgr.getSourceManager().getSLocEntry(SLInfo.first);1131 }1132 1133 std::optional<llvm::MemoryBufferRef> BF =1134 Mgr.getSourceManager().getBufferOrNone(SLInfo.first, SL);1135 if (!BF)1136 return;1137 LangOptions LangOpts;1138 Lexer TheLexer(SL, LangOpts, BF->getBufferStart(),1139 BF->getBufferStart() + SLInfo.second, BF->getBufferEnd());1140 1141 Token I;1142 Token Result; // This will hold the token just before the last ')'1143 int p_count = 0; // This is for parenthesis matching1144 while (!TheLexer.LexFromRawLexer(I)) {1145 if (I.getKind() == tok::l_paren)1146 ++p_count;1147 if (I.getKind() == tok::r_paren) {1148 if (p_count == 1)1149 break;1150 --p_count;1151 }1152 Result = I;1153 }1154 1155 if (isAnyIdentifier(Result.getKind())) {1156 if (Result.getRawIdentifier() == "nil") {1157 reportEmptyContextError(ME);1158 return;1159 }1160 }1161 1162 if (!isStringLiteral(Result.getKind()))1163 return;1164 1165 StringRef Comment =1166 StringRef(Result.getLiteralData(), Result.getLength()).trim('"');1167 1168 if ((Comment.trim().size() == 0 && Comment.size() > 0) || // Is Whitespace1169 Comment.empty()) {1170 reportEmptyContextError(ME);1171 }1172}1173 1174void EmptyLocalizationContextChecker::MethodCrawler::reportEmptyContextError(1175 const ObjCMessageExpr *ME) const {1176 // Generate the bug report.1177 BR.EmitBasicReport(MD, Checker, "Context Missing",1178 "Localizability Issue (Apple)",1179 "Localized string macro should include a non-empty "1180 "comment for translators",1181 PathDiagnosticLocation(ME, BR.getSourceManager(), DCtx));1182}1183 1184namespace {1185class PluralMisuseChecker : public Checker<check::ASTCodeBody> {1186 1187 // A helper class, which walks the AST1188 class MethodCrawler : public DynamicRecursiveASTVisitor {1189 BugReporter &BR;1190 const CheckerBase *Checker;1191 AnalysisDeclContext *AC;1192 1193 // This functions like a stack. We push on any IfStmt or1194 // ConditionalOperator that matches the condition1195 // and pop it off when we leave that statement1196 llvm::SmallVector<const clang::Stmt *, 8> MatchingStatements;1197 // This is true when we are the direct-child of a1198 // matching statement1199 bool InMatchingStatement = false;1200 1201 public:1202 explicit MethodCrawler(BugReporter &InBR, const CheckerBase *Checker,1203 AnalysisDeclContext *InAC)1204 : BR(InBR), Checker(Checker), AC(InAC) {}1205 1206 bool VisitIfStmt(IfStmt *I) override;1207 bool EndVisitIfStmt(IfStmt *I);1208 bool TraverseIfStmt(IfStmt *x) override;1209 bool VisitConditionalOperator(ConditionalOperator *C) override;1210 bool TraverseConditionalOperator(ConditionalOperator *C) override;1211 bool VisitCallExpr(CallExpr *CE) override;1212 bool VisitObjCMessageExpr(ObjCMessageExpr *ME) override;1213 1214 private:1215 void reportPluralMisuseError(const Stmt *S) const;1216 bool isCheckingPlurality(const Expr *E) const;1217 };1218 1219public:1220 void checkASTCodeBody(const Decl *D, AnalysisManager &Mgr,1221 BugReporter &BR) const {1222 MethodCrawler Visitor(BR, this, Mgr.getAnalysisDeclContext(D));1223 Visitor.TraverseDecl(const_cast<Decl *>(D));1224 }1225};1226} // end anonymous namespace1227 1228// Checks the condition of the IfStmt and returns true if one1229// of the following heuristics are met:1230// 1) The conidtion is a variable with "singular" or "plural" in the name1231// 2) The condition is a binary operator with 1 or 2 on the right-hand side1232bool PluralMisuseChecker::MethodCrawler::isCheckingPlurality(1233 const Expr *Condition) const {1234 const BinaryOperator *BO = nullptr;1235 // Accounts for when a VarDecl represents a BinaryOperator1236 if (const DeclRefExpr *DRE = dyn_cast<DeclRefExpr>(Condition)) {1237 if (const VarDecl *VD = dyn_cast<VarDecl>(DRE->getDecl())) {1238 const Expr *InitExpr = VD->getInit();1239 if (InitExpr) {1240 if (const BinaryOperator *B =1241 dyn_cast<BinaryOperator>(InitExpr->IgnoreParenImpCasts())) {1242 BO = B;1243 }1244 }1245 if (VD->getName().contains_insensitive("plural") ||1246 VD->getName().contains_insensitive("singular")) {1247 return true;1248 }1249 }1250 } else if (const BinaryOperator *B = dyn_cast<BinaryOperator>(Condition)) {1251 BO = B;1252 }1253 1254 if (BO == nullptr)1255 return false;1256 1257 if (IntegerLiteral *IL = dyn_cast_or_null<IntegerLiteral>(1258 BO->getRHS()->IgnoreParenImpCasts())) {1259 llvm::APInt Value = IL->getValue();1260 if (Value == 1 || Value == 2) {1261 return true;1262 }1263 }1264 return false;1265}1266 1267// A CallExpr with "LOC" in its identifier that takes in a string literal1268// has been shown to almost always be a function that returns a localized1269// string. Raise a diagnostic when this is in a statement that matches1270// the condition.1271bool PluralMisuseChecker::MethodCrawler::VisitCallExpr(CallExpr *CE) {1272 if (InMatchingStatement) {1273 if (const FunctionDecl *FD = CE->getDirectCallee()) {1274 std::string NormalizedName =1275 StringRef(FD->getNameInfo().getAsString()).lower();1276 if (NormalizedName.find("loc") != std::string::npos) {1277 for (const Expr *Arg : CE->arguments()) {1278 if (isa<ObjCStringLiteral>(Arg))1279 reportPluralMisuseError(CE);1280 }1281 }1282 }1283 }1284 return true;1285}1286 1287// The other case is for NSLocalizedString which also returns1288// a localized string. It's a macro for the ObjCMessageExpr1289// [NSBundle localizedStringForKey:value:table:] Raise a1290// diagnostic when this is in a statement that matches1291// the condition.1292bool PluralMisuseChecker::MethodCrawler::VisitObjCMessageExpr(1293 ObjCMessageExpr *ME) {1294 const ObjCInterfaceDecl *OD = ME->getReceiverInterface();1295 if (!OD)1296 return true;1297 1298 const IdentifierInfo *odInfo = OD->getIdentifier();1299 1300 if (odInfo->isStr("NSBundle") &&1301 ME->getSelector().getAsString() == "localizedStringForKey:value:table:") {1302 if (InMatchingStatement) {1303 reportPluralMisuseError(ME);1304 }1305 }1306 return true;1307}1308 1309/// Override TraverseIfStmt so we know when we are done traversing an IfStmt1310bool PluralMisuseChecker::MethodCrawler::TraverseIfStmt(IfStmt *I) {1311 DynamicRecursiveASTVisitor::TraverseIfStmt(I);1312 return EndVisitIfStmt(I);1313}1314 1315// EndVisit callbacks are not provided by the RecursiveASTVisitor1316// so we override TraverseIfStmt and make a call to EndVisitIfStmt1317// after traversing the IfStmt1318bool PluralMisuseChecker::MethodCrawler::EndVisitIfStmt(IfStmt *I) {1319 MatchingStatements.pop_back();1320 if (!MatchingStatements.empty()) {1321 if (MatchingStatements.back() != nullptr) {1322 InMatchingStatement = true;1323 return true;1324 }1325 }1326 InMatchingStatement = false;1327 return true;1328}1329 1330bool PluralMisuseChecker::MethodCrawler::VisitIfStmt(IfStmt *I) {1331 const Expr *Condition = I->getCond();1332 if (!Condition)1333 return true;1334 Condition = Condition->IgnoreParenImpCasts();1335 if (isCheckingPlurality(Condition)) {1336 MatchingStatements.push_back(I);1337 InMatchingStatement = true;1338 } else {1339 MatchingStatements.push_back(nullptr);1340 InMatchingStatement = false;1341 }1342 1343 return true;1344}1345 1346// Preliminary support for conditional operators.1347bool PluralMisuseChecker::MethodCrawler::TraverseConditionalOperator(1348 ConditionalOperator *C) {1349 DynamicRecursiveASTVisitor::TraverseConditionalOperator(C);1350 MatchingStatements.pop_back();1351 if (!MatchingStatements.empty()) {1352 if (MatchingStatements.back() != nullptr)1353 InMatchingStatement = true;1354 else1355 InMatchingStatement = false;1356 } else {1357 InMatchingStatement = false;1358 }1359 return true;1360}1361 1362bool PluralMisuseChecker::MethodCrawler::VisitConditionalOperator(1363 ConditionalOperator *C) {1364 const Expr *Condition = C->getCond()->IgnoreParenImpCasts();1365 if (isCheckingPlurality(Condition)) {1366 MatchingStatements.push_back(C);1367 InMatchingStatement = true;1368 } else {1369 MatchingStatements.push_back(nullptr);1370 InMatchingStatement = false;1371 }1372 return true;1373}1374 1375void PluralMisuseChecker::MethodCrawler::reportPluralMisuseError(1376 const Stmt *S) const {1377 // Generate the bug report.1378 BR.EmitBasicReport(AC->getDecl(), Checker, "Plural Misuse",1379 "Localizability Issue (Apple)",1380 "Plural cases are not supported across all languages. "1381 "Use a .stringsdict file instead",1382 PathDiagnosticLocation(S, BR.getSourceManager(), AC));1383}1384 1385//===----------------------------------------------------------------------===//1386// Checker registration.1387//===----------------------------------------------------------------------===//1388 1389void ento::registerNonLocalizedStringChecker(CheckerManager &mgr) {1390 NonLocalizedStringChecker *checker =1391 mgr.registerChecker<NonLocalizedStringChecker>();1392 checker->IsAggressive =1393 mgr.getAnalyzerOptions().getCheckerBooleanOption(1394 checker, "AggressiveReport");1395}1396 1397bool ento::shouldRegisterNonLocalizedStringChecker(const CheckerManager &mgr) {1398 return true;1399}1400 1401void ento::registerEmptyLocalizationContextChecker(CheckerManager &mgr) {1402 mgr.registerChecker<EmptyLocalizationContextChecker>();1403}1404 1405bool ento::shouldRegisterEmptyLocalizationContextChecker(1406 const CheckerManager &mgr) {1407 return true;1408}1409 1410void ento::registerPluralMisuseChecker(CheckerManager &mgr) {1411 mgr.registerChecker<PluralMisuseChecker>();1412}1413 1414bool ento::shouldRegisterPluralMisuseChecker(const CheckerManager &mgr) {1415 return true;1416}1417