Index: lib/StaticAnalyzer/Checkers/Checkers.td =================================================================== --- lib/StaticAnalyzer/Checkers/Checkers.td +++ lib/StaticAnalyzer/Checkers/Checkers.td @@ -464,6 +464,10 @@ HelpText<"Check that NSLocalizedString macros include a comment for context">, DescFile<"LocalizationChecker.cpp">; +def PluralMisuseChecker : Checker<"PluralMisuseChecker">, + HelpText<"Warns against using one vs. many plural pattern in code when generating localized strings, as this doesn't work for many non-English languages.">, + DescFile<"LocalizationChecker.cpp">; + } // end "alpha.osx.cocoa" let ParentPackage = CoreFoundation in { Index: lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp =================================================================== --- lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp +++ lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp @@ -84,6 +84,9 @@ void reportLocalizationError(SVal S, const ObjCMethodCall &M, CheckerContext &C, int argumentNumber = 0) const; + int getLocalizedArgumentForSelector(StringRef Receiver, + StringRef SelectorName) const; + public: NonLocalizedStringChecker(); @@ -104,7 +107,8 @@ LocalizedState) NonLocalizedStringChecker::NonLocalizedStringChecker() { - BT.reset(new BugType(this, "Unlocalized string", "Localizability Error")); + BT.reset(new BugType(this, "Unlocalizable string", + "Localizability Issue (Apple)")); } /// Initializes a list of methods that require a localized string @@ -113,17 +117,86 @@ if (!UIMethods.empty()) return; - // TODO: This should eventually be a comprehensive list of UIKit methods + // UISearchDisplayController Methods + llvm::StringMap &UISearchDisplayControllerM = + UIMethods.insert( + {"UISearchDisplayController", llvm::StringMap()}) + .first->second; + UISearchDisplayControllerM.insert({"setSearchResultsTitle:", 0}); - // UILabel Methods - llvm::StringMap &UILabelM = - UIMethods.insert({"UILabel", llvm::StringMap()}).first->second; - UILabelM.insert({"setText:", 0}); + // UITabBarItem Methods + llvm::StringMap &UITabBarItemM = + UIMethods.insert({"UITabBarItem", llvm::StringMap()}) + .first->second; + UITabBarItemM.insert({"initWithTitle:image:tag:", 0}); + UITabBarItemM.insert({"initWithTitle:image:selectedImage:", 0}); - // UIButton Methods - llvm::StringMap &UIButtonM = - UIMethods.insert({"UIButton", llvm::StringMap()}).first->second; - UIButtonM.insert({"setText:", 0}); + // NSDockTile Methods + llvm::StringMap &NSDockTileM = + UIMethods.insert({"NSDockTile", llvm::StringMap()}) + .first->second; + NSDockTileM.insert({"setBadgeLabel:", 0}); + + // NSStatusItem Methods + llvm::StringMap &NSStatusItemM = + UIMethods.insert({"NSStatusItem", llvm::StringMap()}) + .first->second; + NSStatusItemM.insert({"setTitle:", 0}); + NSStatusItemM.insert({"setToolTip:", 0}); + + // UITableViewRowAction Methods + llvm::StringMap &UITableViewRowActionM = + UIMethods.insert({"UITableViewRowAction", llvm::StringMap()}) + .first->second; + UITableViewRowActionM.insert({"rowActionWithStyle:title:handler:", 1}); + UITableViewRowActionM.insert({"setTitle:", 0}); + + // NSBox Methods + llvm::StringMap &NSBoxM = + UIMethods.insert({"NSBox", llvm::StringMap()}).first->second; + NSBoxM.insert({"setTitle:", 0}); + + // NSButton Methods + llvm::StringMap &NSButtonM = + UIMethods.insert({"NSButton", llvm::StringMap()}).first->second; + NSButtonM.insert({"setTitle:", 0}); + NSButtonM.insert({"setAlternateTitle:", 0}); + + // NSSavePanel Methods + llvm::StringMap &NSSavePanelM = + UIMethods.insert({"NSSavePanel", llvm::StringMap()}) + .first->second; + NSSavePanelM.insert({"setPrompt:", 0}); + NSSavePanelM.insert({"setTitle:", 0}); + NSSavePanelM.insert({"setNameFieldLabel:", 0}); + NSSavePanelM.insert({"setNameFieldStringValue:", 0}); + NSSavePanelM.insert({"setMessage:", 0}); + + // UIPrintInfo Methods + llvm::StringMap &UIPrintInfoM = + UIMethods.insert({"UIPrintInfo", llvm::StringMap()}) + .first->second; + UIPrintInfoM.insert({"setJobName:", 0}); + + // NSTabViewItem Methods + llvm::StringMap &NSTabViewItemM = + UIMethods.insert({"NSTabViewItem", llvm::StringMap()}) + .first->second; + NSTabViewItemM.insert({"setLabel:", 0}); + NSTabViewItemM.insert({"setToolTip:", 0}); + + // NSBrowser Methods + llvm::StringMap &NSBrowserM = + UIMethods.insert({"NSBrowser", llvm::StringMap()}).first->second; + NSBrowserM.insert({"setTitle:ofColumn:", 0}); + + // UIAccessibilityElement Methods + llvm::StringMap &UIAccessibilityElementM = + UIMethods.insert({"UIAccessibilityElement", llvm::StringMap()}) + .first->second; + UIAccessibilityElementM.insert({"setAccessibilityLabel:", 0}); + UIAccessibilityElementM.insert({"setAccessibilityHint:", 0}); + UIAccessibilityElementM.insert({"setAccessibilityValue:", 0}); // UIAlertAction Methods llvm::StringMap &UIAlertActionM = @@ -131,29 +204,204 @@ .first->second; UIAlertActionM.insert({"actionWithTitle:style:handler:", 0}); + // NSPopUpButton Methods + llvm::StringMap &NSPopUpButtonM = + UIMethods.insert({"NSPopUpButton", llvm::StringMap()}) + .first->second; + NSPopUpButtonM.insert({"addItemWithTitle:", 0}); + NSPopUpButtonM.insert({"insertItemWithTitle:atIndex:", 0}); + NSPopUpButtonM.insert({"removeItemWithTitle:", 0}); + NSPopUpButtonM.insert({"selectItemWithTitle:", 0}); + NSPopUpButtonM.insert({"setTitle:", 0}); + + // NSTableViewRowAction Methods + llvm::StringMap &NSTableViewRowActionM = + UIMethods.insert({"NSTableViewRowAction", llvm::StringMap()}) + .first->second; + NSTableViewRowActionM.insert({"rowActionWithStyle:title:handler:", 1}); + NSTableViewRowActionM.insert({"setTitle:", 0}); + + // NSImage Methods + llvm::StringMap &NSImageM = + UIMethods.insert({"NSImage", llvm::StringMap()}).first->second; + NSImageM.insert({"setAccessibilityDescription:", 0}); + + // NSUserActivity Methods + llvm::StringMap &NSUserActivityM = + UIMethods.insert({"NSUserActivity", llvm::StringMap()}) + .first->second; + NSUserActivityM.insert({"setTitle:", 0}); + + // NSPathControlItem Methods + llvm::StringMap &NSPathControlItemM = + UIMethods.insert({"NSPathControlItem", llvm::StringMap()}) + .first->second; + NSPathControlItemM.insert({"setTitle:", 0}); + + // NSCell Methods + llvm::StringMap &NSCellM = + UIMethods.insert({"NSCell", llvm::StringMap()}).first->second; + NSCellM.insert({"initTextCell:", 0}); + NSCellM.insert({"setTitle:", 0}); + NSCellM.insert({"setStringValue:", 0}); + + // NSPathControl Methods + llvm::StringMap &NSPathControlM = + UIMethods.insert({"NSPathControl", llvm::StringMap()}) + .first->second; + NSPathControlM.insert({"setPlaceholderString:", 0}); + + // UIAccessibility Methods + llvm::StringMap &UIAccessibilityM = + UIMethods.insert({"UIAccessibility", llvm::StringMap()}) + .first->second; + UIAccessibilityM.insert({"setAccessibilityLabel:", 0}); + UIAccessibilityM.insert({"setAccessibilityHint:", 0}); + UIAccessibilityM.insert({"setAccessibilityValue:", 0}); + + // NSTableColumn Methods + llvm::StringMap &NSTableColumnM = + UIMethods.insert({"NSTableColumn", llvm::StringMap()}) + .first->second; + NSTableColumnM.insert({"setTitle:", 0}); + NSTableColumnM.insert({"setHeaderToolTip:", 0}); + + // NSSegmentedControl Methods + llvm::StringMap &NSSegmentedControlM = + UIMethods.insert({"NSSegmentedControl", llvm::StringMap()}) + .first->second; + NSSegmentedControlM.insert({"setLabel:forSegment:", 0}); + + // NSButtonCell Methods + llvm::StringMap &NSButtonCellM = + UIMethods.insert({"NSButtonCell", llvm::StringMap()}) + .first->second; + NSButtonCellM.insert({"setTitle:", 0}); + NSButtonCellM.insert({"setAlternateTitle:", 0}); + + // NSSliderCell Methods + llvm::StringMap &NSSliderCellM = + UIMethods.insert({"NSSliderCell", llvm::StringMap()}) + .first->second; + NSSliderCellM.insert({"setTitle:", 0}); + + // NSControl Methods + llvm::StringMap &NSControlM = + UIMethods.insert({"NSControl", llvm::StringMap()}).first->second; + NSControlM.insert({"setStringValue:", 0}); + + // NSAccessibility Methods + llvm::StringMap &NSAccessibilityM = + UIMethods.insert({"NSAccessibility", llvm::StringMap()}) + .first->second; + NSAccessibilityM.insert({"setAccessibilityValueDescription:", 0}); + NSAccessibilityM.insert({"setAccessibilityLabel:", 0}); + NSAccessibilityM.insert({"setAccessibilityTitle:", 0}); + NSAccessibilityM.insert({"setAccessibilityPlaceholderValue:", 0}); + NSAccessibilityM.insert({"setAccessibilityHelp:", 0}); + + // NSMatrix Methods + llvm::StringMap &NSMatrixM = + UIMethods.insert({"NSMatrix", llvm::StringMap()}).first->second; + NSMatrixM.insert({"setToolTip:forCell:", 0}); + + // NSPrintPanel Methods + llvm::StringMap &NSPrintPanelM = + UIMethods.insert({"NSPrintPanel", llvm::StringMap()}) + .first->second; + NSPrintPanelM.insert({"setDefaultButtonTitle:", 0}); + + // UILocalNotification Methods + llvm::StringMap &UILocalNotificationM = + UIMethods.insert({"UILocalNotification", llvm::StringMap()}) + .first->second; + UILocalNotificationM.insert({"setAlertBody:", 0}); + UILocalNotificationM.insert({"setAlertAction:", 0}); + UILocalNotificationM.insert({"setAlertTitle:", 0}); + + // NSSlider Methods + llvm::StringMap &NSSliderM = + UIMethods.insert({"NSSlider", llvm::StringMap()}).first->second; + NSSliderM.insert({"setTitle:", 0}); + + // UIMenuItem Methods + llvm::StringMap &UIMenuItemM = + UIMethods.insert({"UIMenuItem", llvm::StringMap()}) + .first->second; + UIMenuItemM.insert({"initWithTitle:action:", 0}); + UIMenuItemM.insert({"setTitle:", 0}); + // UIAlertController Methods llvm::StringMap &UIAlertControllerM = UIMethods.insert({"UIAlertController", llvm::StringMap()}) .first->second; UIAlertControllerM.insert( {"alertControllerWithTitle:message:preferredStyle:", 1}); + UIAlertControllerM.insert({"setTitle:", 0}); + UIAlertControllerM.insert({"setMessage:", 0}); - // NSButton Methods - llvm::StringMap &NSButtonM = - UIMethods.insert({"NSButton", llvm::StringMap()}).first->second; - NSButtonM.insert({"setTitle:", 0}); + // UIApplicationShortcutItem Methods + llvm::StringMap &UIApplicationShortcutItemM = + UIMethods.insert( + {"UIApplicationShortcutItem", llvm::StringMap()}) + .first->second; + UIApplicationShortcutItemM.insert( + {"initWithType:localizedTitle:localizedSubtitle:icon:userInfo:", 1}); + UIApplicationShortcutItemM.insert({"initWithType:localizedTitle:", 1}); - // NSButtonCell Methods - llvm::StringMap &NSButtonCellM = - UIMethods.insert({"NSButtonCell", llvm::StringMap()}) + // UIActionSheet Methods + llvm::StringMap &UIActionSheetM = + UIMethods.insert({"UIActionSheet", llvm::StringMap()}) .first->second; - NSButtonCellM.insert({"setTitle:", 0}); + UIActionSheetM.insert({"initWithTitle:delegate:cancelButtonTitle:" + "destructiveButtonTitle:otherButtonTitles:", + 0}); + UIActionSheetM.insert({"addButtonWithTitle:", 0}); + UIActionSheetM.insert({"setTitle:", 0}); + + // NSURLSessionTask Methods + llvm::StringMap &NSURLSessionTaskM = + UIMethods.insert({"NSURLSessionTask", llvm::StringMap()}) + .first->second; + NSURLSessionTaskM.insert({"setTaskDescription:", 0}); - // NSMenuItem Methods - llvm::StringMap &NSMenuItemM = - UIMethods.insert({"NSMenuItem", llvm::StringMap()}) + // UIAccessibilityCustomAction Methods + llvm::StringMap &UIAccessibilityCustomActionM = + UIMethods.insert( + {"UIAccessibilityCustomAction", llvm::StringMap()}) .first->second; - NSMenuItemM.insert({"setTitle:", 0}); + UIAccessibilityCustomActionM.insert({"initWithName:target:selector:", 0}); + UIAccessibilityCustomActionM.insert({"setName:", 0}); + + // UISearchBar Methods + llvm::StringMap &UISearchBarM = + UIMethods.insert({"UISearchBar", llvm::StringMap()}) + .first->second; + UISearchBarM.insert({"setText:", 0}); + UISearchBarM.insert({"setPrompt:", 0}); + UISearchBarM.insert({"setPlaceholder:", 0}); + + // UIBarItem Methods + llvm::StringMap &UIBarItemM = + UIMethods.insert({"UIBarItem", llvm::StringMap()}).first->second; + UIBarItemM.insert({"setTitle:", 0}); + + // UITextView Methods + llvm::StringMap &UITextViewM = + UIMethods.insert({"UITextView", llvm::StringMap()}) + .first->second; + UITextViewM.insert({"setText:", 0}); + + // NSView Methods + llvm::StringMap &NSViewM = + UIMethods.insert({"NSView", llvm::StringMap()}).first->second; + NSViewM.insert({"setToolTip:", 0}); + + // NSTextField Methods + llvm::StringMap &NSTextFieldM = + UIMethods.insert({"NSTextField", llvm::StringMap()}) + .first->second; + NSTextFieldM.insert({"setPlaceholderString:", 0}); // NSAttributedString Methods llvm::StringMap &NSAttributedStringM = @@ -161,6 +409,227 @@ .first->second; NSAttributedStringM.insert({"initWithString:", 0}); NSAttributedStringM.insert({"initWithString:attributes:", 0}); + + // NSText Methods + llvm::StringMap &NSTextM = + UIMethods.insert({"NSText", llvm::StringMap()}).first->second; + NSTextM.insert({"setString:", 0}); + + // UIKeyCommand Methods + llvm::StringMap &UIKeyCommandM = + UIMethods.insert({"UIKeyCommand", llvm::StringMap()}) + .first->second; + UIKeyCommandM.insert( + {"keyCommandWithInput:modifierFlags:action:discoverabilityTitle:", 3}); + UIKeyCommandM.insert({"setDiscoverabilityTitle:", 0}); + + // UILabel Methods + llvm::StringMap &UILabelM = + UIMethods.insert({"UILabel", llvm::StringMap()}).first->second; + UILabelM.insert({"setText:", 0}); + + // NSAlert Methods + llvm::StringMap &NSAlertM = + UIMethods.insert({"NSAlert", llvm::StringMap()}).first->second; + NSAlertM.insert({"alertWithMessageText:defaultButton:alternateButton:" + "otherButton:informativeTextWithFormat:", + 0}); + NSAlertM.insert({"addButtonWithTitle:", 0}); + NSAlertM.insert({"setMessageText:", 0}); + NSAlertM.insert({"setInformativeText:", 0}); + NSAlertM.insert({"setHelpAnchor:", 0}); + + // UIMutableApplicationShortcutItem Methods + llvm::StringMap &UIMutableApplicationShortcutItemM = + UIMethods.insert({"UIMutableApplicationShortcutItem", + llvm::StringMap()}) + .first->second; + UIMutableApplicationShortcutItemM.insert({"setLocalizedTitle:", 0}); + UIMutableApplicationShortcutItemM.insert({"setLocalizedSubtitle:", 0}); + + // UIButton Methods + llvm::StringMap &UIButtonM = + UIMethods.insert({"UIButton", llvm::StringMap()}).first->second; + UIButtonM.insert({"setTitle:forState:", 0}); + + // NSWindow Methods + llvm::StringMap &NSWindowM = + UIMethods.insert({"NSWindow", llvm::StringMap()}).first->second; + NSWindowM.insert({"setTitle:", 0}); + NSWindowM.insert({"minFrameWidthWithTitle:styleMask:", 0}); + NSWindowM.insert({"setMiniwindowTitle:", 0}); + + // NSPathCell Methods + llvm::StringMap &NSPathCellM = + UIMethods.insert({"NSPathCell", llvm::StringMap()}) + .first->second; + NSPathCellM.insert({"setPlaceholderString:", 0}); + + // UIDocumentMenuViewController Methods + llvm::StringMap &UIDocumentMenuViewControllerM = + UIMethods.insert( + {"UIDocumentMenuViewController", llvm::StringMap()}) + .first->second; + UIDocumentMenuViewControllerM.insert( + {"addOptionWithTitle:image:order:handler:", 0}); + + // UINavigationItem Methods + llvm::StringMap &UINavigationItemM = + UIMethods.insert({"UINavigationItem", llvm::StringMap()}) + .first->second; + UINavigationItemM.insert({"initWithTitle:", 0}); + UINavigationItemM.insert({"setTitle:", 0}); + UINavigationItemM.insert({"setPrompt:", 0}); + + // UIAlertView Methods + llvm::StringMap &UIAlertViewM = + UIMethods.insert({"UIAlertView", llvm::StringMap()}) + .first->second; + UIAlertViewM.insert( + {"initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:", + 0}); + UIAlertViewM.insert({"addButtonWithTitle:", 0}); + UIAlertViewM.insert({"setTitle:", 0}); + UIAlertViewM.insert({"setMessage:", 0}); + + // NSFormCell Methods + llvm::StringMap &NSFormCellM = + UIMethods.insert({"NSFormCell", llvm::StringMap()}) + .first->second; + NSFormCellM.insert({"initTextCell:", 0}); + NSFormCellM.insert({"setTitle:", 0}); + NSFormCellM.insert({"setPlaceholderString:", 0}); + + // NSUserNotification Methods + llvm::StringMap &NSUserNotificationM = + UIMethods.insert({"NSUserNotification", llvm::StringMap()}) + .first->second; + NSUserNotificationM.insert({"setTitle:", 0}); + NSUserNotificationM.insert({"setSubtitle:", 0}); + NSUserNotificationM.insert({"setInformativeText:", 0}); + NSUserNotificationM.insert({"setActionButtonTitle:", 0}); + NSUserNotificationM.insert({"setOtherButtonTitle:", 0}); + NSUserNotificationM.insert({"setResponsePlaceholder:", 0}); + + // NSToolbarItem Methods + llvm::StringMap &NSToolbarItemM = + UIMethods.insert({"NSToolbarItem", llvm::StringMap()}) + .first->second; + NSToolbarItemM.insert({"setLabel:", 0}); + NSToolbarItemM.insert({"setPaletteLabel:", 0}); + NSToolbarItemM.insert({"setToolTip:", 0}); + + // NSProgress Methods + llvm::StringMap &NSProgressM = + UIMethods.insert({"NSProgress", llvm::StringMap()}) + .first->second; + NSProgressM.insert({"setLocalizedDescription:", 0}); + NSProgressM.insert({"setLocalizedAdditionalDescription:", 0}); + + // NSSegmentedCell Methods + llvm::StringMap &NSSegmentedCellM = + UIMethods.insert({"NSSegmentedCell", llvm::StringMap()}) + .first->second; + NSSegmentedCellM.insert({"setLabel:forSegment:", 0}); + NSSegmentedCellM.insert({"setToolTip:forSegment:", 0}); + + // NSUndoManager Methods + llvm::StringMap &NSUndoManagerM = + UIMethods.insert({"NSUndoManager", llvm::StringMap()}) + .first->second; + NSUndoManagerM.insert({"setActionName:", 0}); + NSUndoManagerM.insert({"undoMenuTitleForUndoActionName:", 0}); + NSUndoManagerM.insert({"redoMenuTitleForUndoActionName:", 0}); + + // NSMenuItem Methods + llvm::StringMap &NSMenuItemM = + UIMethods.insert({"NSMenuItem", llvm::StringMap()}) + .first->second; + NSMenuItemM.insert({"initWithTitle:action:keyEquivalent:", 0}); + NSMenuItemM.insert({"setTitle:", 0}); + NSMenuItemM.insert({"setToolTip:", 0}); + + // NSPopUpButtonCell Methods + llvm::StringMap &NSPopUpButtonCellM = + UIMethods.insert({"NSPopUpButtonCell", llvm::StringMap()}) + .first->second; + NSPopUpButtonCellM.insert({"initTextCell:pullsDown:", 0}); + NSPopUpButtonCellM.insert({"addItemWithTitle:", 0}); + NSPopUpButtonCellM.insert({"insertItemWithTitle:atIndex:", 0}); + NSPopUpButtonCellM.insert({"removeItemWithTitle:", 0}); + NSPopUpButtonCellM.insert({"selectItemWithTitle:", 0}); + NSPopUpButtonCellM.insert({"setTitle:", 0}); + + // NSViewController Methods + llvm::StringMap &NSViewControllerM = + UIMethods.insert({"NSViewController", llvm::StringMap()}) + .first->second; + NSViewControllerM.insert({"setTitle:", 0}); + + // NSMenu Methods + llvm::StringMap &NSMenuM = + UIMethods.insert({"NSMenu", llvm::StringMap()}).first->second; + NSMenuM.insert({"initWithTitle:", 0}); + NSMenuM.insert({"insertItemWithTitle:action:keyEquivalent:atIndex:", 0}); + NSMenuM.insert({"addItemWithTitle:action:keyEquivalent:", 0}); + NSMenuM.insert({"setTitle:", 0}); + + // UIMutableUserNotificationAction Methods + llvm::StringMap &UIMutableUserNotificationActionM = + UIMethods.insert({"UIMutableUserNotificationAction", + llvm::StringMap()}) + .first->second; + UIMutableUserNotificationActionM.insert({"setTitle:", 0}); + + // NSForm Methods + llvm::StringMap &NSFormM = + UIMethods.insert({"NSForm", llvm::StringMap()}).first->second; + NSFormM.insert({"addEntry:", 0}); + NSFormM.insert({"insertEntry:atIndex:", 0}); + + // NSTextFieldCell Methods + llvm::StringMap &NSTextFieldCellM = + UIMethods.insert({"NSTextFieldCell", llvm::StringMap()}) + .first->second; + NSTextFieldCellM.insert({"setPlaceholderString:", 0}); + + // NSUserNotificationAction Methods + llvm::StringMap &NSUserNotificationActionM = + UIMethods.insert({"NSUserNotificationAction", llvm::StringMap()}) + .first->second; + NSUserNotificationActionM.insert({"actionWithIdentifier:title:", 1}); + + // NSURLSession Methods + llvm::StringMap &NSURLSessionM = + UIMethods.insert({"NSURLSession", llvm::StringMap()}) + .first->second; + NSURLSessionM.insert({"setSessionDescription:", 0}); + + // UITextField Methods + llvm::StringMap &UITextFieldM = + UIMethods.insert({"UITextField", llvm::StringMap()}) + .first->second; + UITextFieldM.insert({"setText:", 0}); + UITextFieldM.insert({"setPlaceholder:", 0}); + + // UIBarButtonItem Methods + llvm::StringMap &UIBarButtonItemM = + UIMethods.insert({"UIBarButtonItem", llvm::StringMap()}) + .first->second; + UIBarButtonItemM.insert({"initWithTitle:style:target:action:", 0}); + + // UIViewController Methods + llvm::StringMap &UIViewControllerM = + UIMethods.insert({"UIViewController", llvm::StringMap()}) + .first->second; + UIViewControllerM.insert({"setTitle:", 0}); + + // UISegmentedControl Methods + llvm::StringMap &UISegmentedControlM = + UIMethods.insert({"UISegmentedControl", llvm::StringMap()}) + .first->second; + UISegmentedControlM.insert({"insertSegmentWithTitle:atIndex:animated:", 0}); + UISegmentedControlM.insert({"setTitle:forSegmentAtIndex:", 0}); } /// Initializes a list of methods and C functions that return a localized string @@ -185,6 +654,8 @@ /// Checks to see if the method / function declaration includes /// __attribute__((annotate("returns_localized_nsstring"))) bool NonLocalizedStringChecker::isAnnotatedAsLocalized(const Decl *D) const { + if (!D) + return false; return std::any_of( D->specific_attr_begin(), D->specific_attr_end(), [](const AnnotateAttr *Ann) { @@ -253,8 +724,8 @@ return; // Generate the bug report. - std::unique_ptr R( - new BugReport(*BT, "String should be localized", ErrNode)); + std::unique_ptr R(new BugReport( + *BT, "User-facing text should use localized string macro", ErrNode)); if (argumentNumber) { R->addRange(M.getArgExpr(argumentNumber - 1)->getSourceRange()); } else { @@ -264,6 +735,24 @@ C.emitReport(std::move(R)); } +/// Returns the argument number requiring localized string if it exists +/// otherwise, returns -1 +int NonLocalizedStringChecker::getLocalizedArgumentForSelector( + StringRef Receiver, StringRef SelectorName) const { + auto method = UIMethods.find(Receiver); + + if (method == UIMethods.end()) + return -1; + + auto argumentIterator = method->getValue().find(SelectorName); + + if (argumentIterator == method->getValue().end()) + return -1; + + int argumentNumber = argumentIterator->getValue(); + return argumentNumber; +} + /// Check if the string being passed in has NonLocalized state void NonLocalizedStringChecker::checkPreObjCMessage(const ObjCMethodCall &msg, CheckerContext &C) const { @@ -280,7 +769,6 @@ 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 @@ -297,33 +785,45 @@ 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 = + getLocalizedArgumentForSelector(odInfo->getName(), SelectorName); + // Go up each hierarchy of superclasses and their protocols + while (argumentNumber < 0 && OD->getSuperClass() != nullptr) { + for (const auto *P : OD->all_referenced_protocols()) { + argumentNumber = getLocalizedArgumentForSelector( + P->getIdentifier()->getName(), SelectorName); + if (argumentNumber >= 0) + break; + } + if (argumentNumber < 0) { + OD = OD->getSuperClass(); + argumentNumber = getLocalizedArgumentForSelector( + OD->getIdentifier()->getName(), SelectorName); + } + } - int argumentNumber = argumentIterator->getValue(); + if (argumentNumber < 0) // There was no match in UIMethods + return; - SVal svTitle = msg.getArgSVal(argumentNumber); + SVal svTitle = msg.getArgSVal(argumentNumber); - if (const ObjCStringRegion *SR = - dyn_cast_or_null(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; - } + if (const ObjCStringRegion *SR = + dyn_cast_or_null(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); + bool isNonLocalized = hasNonLocalizedState(svTitle, C); - if (isNonLocalized) { - reportLocalizationError(svTitle, msg, C, argumentNumber + 1); - } + if (isNonLocalized) { + reportLocalizationError(svTitle, msg, C, argumentNumber + 1); } } @@ -505,7 +1005,7 @@ const IdentifierInfo *odInfo = OD->getIdentifier(); - if (!(odInfo->isStr("NSBundle") || + if (!(odInfo->isStr("NSBundle") && ME->getSelector().getAsString() == "localizedStringForKey:value:table:")) { return; @@ -573,12 +1073,204 @@ void EmptyLocalizationContextChecker::MethodCrawler::reportEmptyContextError( const ObjCMessageExpr *ME) const { // Generate the bug report. - BR.EmitBasicReport(MD, Checker, "Context Missing", "Localizability Error", + BR.EmitBasicReport(MD, Checker, "Context Missing", + "Localizability Issue (Apple)", "Localized string macro should include a non-empty " "comment for translators", PathDiagnosticLocation(ME, BR.getSourceManager(), DCtx)); } +namespace { +class PluralMisuseChecker : public Checker { + + // A helper class, which walks the AST + class MethodCrawler : public RecursiveASTVisitor { + BugReporter &BR; + const CheckerBase *Checker; + AnalysisDeclContext *AC; + + // This functions like a stack. We push on any IfStmt or + // ConditionalOperator that matches the condition + // and pop it off when we leave that statement + llvm::SmallVector MatchingStatements; + // This is true when we are the direct-child of a + // matching statement + bool InMatchingStatement = false; + + public: + explicit MethodCrawler(BugReporter &InBR, const CheckerBase *Checker, + AnalysisDeclContext *InAC) + : BR(InBR), Checker(Checker), AC(InAC) {} + + bool VisitIfStmt(const IfStmt *I); + bool TraverseIfStmt(IfStmt *x); + bool VisitConditionalOperator(const ConditionalOperator *C); + bool TraverseConditionalOperator(ConditionalOperator *C); + bool VisitCallExpr(const CallExpr *CE); + bool VisitObjCMessageExpr(const ObjCMessageExpr *ME); + + private: + void reportPluralMisuseError(const Stmt *S) const; + bool checkCondition(const Expr *E) const; + }; + +public: + void checkASTCodeBody(const Decl *D, AnalysisManager &Mgr, + BugReporter &BR) const { + MethodCrawler Visitor(BR, this, Mgr.getAnalysisDeclContext(D)); + Visitor.TraverseDecl(const_cast(D)); + } +}; +} // end anonymous namespace + +// Checks the condition of the IfStmt and returns true if one +// of the following heuristics are met: +// 1) The conidtion is a variable with "singular" or "plural" in the name +// 2) The condition is a binary operator with 1 or 2 on the right-hand side +bool PluralMisuseChecker::MethodCrawler::checkCondition( + const Expr *Condition) const { + const BinaryOperator *BO = nullptr; + // Accounts for when a VarDecl represents a BinaryOperator + if (const DeclRefExpr *DRE = dyn_cast(Condition)) { + if (const VarDecl *VD = dyn_cast(DRE->getDecl())) { + const Expr *InitExpr = VD->getInit(); + if (InitExpr) { + if (const BinaryOperator *B = + dyn_cast(InitExpr->IgnoreParenImpCasts())) { + BO = B; + } + } + if (VD->getName().lower().find("plural") != StringRef::npos || + VD->getName().lower().find("singular") != StringRef::npos) { + llvm::errs() << VD->getName().lower() << "\n"; + return true; + } + } + } else if (const BinaryOperator *B = dyn_cast(Condition)) { + BO = B; + } + + if (BO == nullptr) + return false; + + if (IntegerLiteral *IL = dyn_cast_or_null( + BO->getRHS()->IgnoreParenImpCasts())) { + llvm::APInt Value = IL->getValue(); + if (Value == 1 || Value == 2) { + return true; + } + } + return false; +} + +// A CallExpr with "LOC" in its identifier that takes in a string literal +// has been shown to almost always be a function that returns a localized +// string. Raise a diagnostic when this is in a statement that matches +// the condition. +bool PluralMisuseChecker::MethodCrawler::VisitCallExpr(const CallExpr *CE) { + if (InMatchingStatement) { + if (const FunctionDecl *FD = CE->getDirectCallee()) { + StringRef NormalizedName = + StringRef(FD->getNameInfo().getAsString()).lower(); + if (NormalizedName.find("loc") != StringRef::npos) { + for (const Expr *Arg : CE->arguments()) { + if (isa(Arg)) + reportPluralMisuseError(CE); + } + } + } + } + return true; +} + +// The other case is for NSLocalizedString which also returns +// a localized string. It's a macro for the ObjCMessageExpr +// [NSBundle localizedStringForKey:value:table:] Raise a +// diagnostic when this is in a statement that matches +// the condition. +bool PluralMisuseChecker::MethodCrawler::VisitObjCMessageExpr( + const ObjCMessageExpr *ME) { + const ObjCInterfaceDecl *OD = ME->getReceiverInterface(); + if (!OD) + return true; + + const IdentifierInfo *odInfo = OD->getIdentifier(); + + if (odInfo->isStr("NSBundle") && + ME->getSelector().getAsString() == "localizedStringForKey:value:table:") { + if (InMatchingStatement) { + reportPluralMisuseError(ME); + } + } + return true; +} + +bool PluralMisuseChecker::MethodCrawler::TraverseIfStmt(IfStmt *I) { + RecursiveASTVisitor::TraverseIfStmt(I); + MatchingStatements.pop_back(); + if (!MatchingStatements.empty()) { + if (MatchingStatements.back() != nullptr) + InMatchingStatement = true; + else + InMatchingStatement = false; + } else { + InMatchingStatement = false; + } + return true; +} + +bool PluralMisuseChecker::MethodCrawler::VisitIfStmt(const IfStmt *I) { + const Expr *Condition = I->getCond()->IgnoreParenImpCasts(); + if (checkCondition(Condition)) { + MatchingStatements.push_back(I); + InMatchingStatement = true; + } else { + MatchingStatements.push_back(nullptr); + InMatchingStatement = false; + } + + return true; +} + +// Preliminary support for conditional operators. +bool PluralMisuseChecker::MethodCrawler::TraverseConditionalOperator( + ConditionalOperator *C) { + RecursiveASTVisitor::TraverseConditionalOperator(C); + MatchingStatements.pop_back(); + if (!MatchingStatements.empty()) { + if (MatchingStatements.back() != nullptr) + InMatchingStatement = true; + else + InMatchingStatement = false; + } else { + InMatchingStatement = false; + } + return true; +} + +bool PluralMisuseChecker::MethodCrawler::VisitConditionalOperator( + const ConditionalOperator *C) { + const Expr *Condition = C->getCond()->IgnoreParenImpCasts(); + if (checkCondition(Condition)) { + MatchingStatements.push_back(C); + InMatchingStatement = true; + } else { + MatchingStatements.push_back(nullptr); + InMatchingStatement = false; + } + return true; +} + +void PluralMisuseChecker::MethodCrawler::reportPluralMisuseError( + const Stmt *S) const { + // Generate the bug report. + BR.EmitBasicReport(AC->getDecl(), Checker, "Plural Misuse", + "Localizability Issue (Apple)", + "Plural cases are not supported accross all languages. " + "Use a .stringsdict file", + PathDiagnosticLocation(S, BR.getSourceManager(), AC)); +} + //===----------------------------------------------------------------------===// // Checker registration. //===----------------------------------------------------------------------===// @@ -592,4 +1284,8 @@ void ento::registerEmptyLocalizationContextChecker(CheckerManager &mgr) { mgr.registerChecker(); +} + +void ento::registerPluralMisuseChecker(CheckerManager &mgr) { + mgr.registerChecker(); } \ No newline at end of file Index: test/Analysis/localization-aggressive.m =================================================================== --- test/Analysis/localization-aggressive.m +++ test/Analysis/localization-aggressive.m @@ -34,13 +34,21 @@ value:(NSString *)value table:(NSString *)tableName; @end -@interface UILabel : NSObject -@property(nullable, nonatomic, copy) NSString *text; +@protocol UIAccessibility - (void)accessibilitySetIdentification:(NSString *)ident; +- (void)setAccessibilityLabel:(NSString *)label; +@end +@interface UILabel : NSObject +@property(nullable, nonatomic, copy) NSString *text; @end @interface TestObject : NSObject @property(strong) NSString *text; @end +@interface NSView : NSObject +@property (strong) NSString *toolTip; +@end +@interface NSViewSubclass : NSView +@end @interface LocalizationTestSuite : NSObject NSString *ForceLocalized(NSString *str) @@ -78,7 +86,7 @@ bar = @"Unlocalized string"; } - [testLabel setText:bar]; // expected-warning {{String should be localized}} + [testLabel setText:bar]; // expected-warning {{User-facing text should use localized string macro}} } - (void)testLocalizationErrorDetectedOnNSString { @@ -88,7 +96,7 @@ bar = @"Unlocalized string"; } - [bar drawAtPoint:CGPointMake(0, 0) withAttributes:nil]; // expected-warning {{String should be localized}} + [bar drawAtPoint:CGPointMake(0, 0) withAttributes:nil]; // expected-warning {{User-facing text should use localized string macro}} } - (void)testNoLocalizationErrorDetectedFromCFunction { @@ -137,7 +145,7 @@ // 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}} + [testLabel setText:@"Hello"]; // expected-warning {{User-facing text should use localized string macro}} } // A nil string should not raise an error @@ -176,7 +184,7 @@ - (void)localizedStringAsArgument:(NSString *)argumentString { UILabel *testLabel = [[UILabel alloc] init]; - [testLabel setText:argumentString]; // expected-warning {{String should be localized}} + [testLabel setText:argumentString]; // expected-warning {{User-facing text should use localized string macro}} } // [LocalizationTestSuite unLocalizedStringMethod] returns an unlocalized string @@ -187,7 +195,7 @@ UILabel *testLabel = [[UILabel alloc] init]; NSString *bar = NSLocalizedString(@"Hello", @"Comment"); - [testLabel setText:[LocalizationTestSuite unLocalizedStringMethod]]; // expected-warning {{String should be localized}} + [testLabel setText:[LocalizationTestSuite unLocalizedStringMethod]]; // expected-warning {{User-facing text should use localized string macro}} } // This is the reverse situation: accessibilitySetIdentification: doesn't care @@ -198,6 +206,21 @@ [testLabel accessibilitySetIdentification:@"UnlocalizedString"]; // no-warning } +// An NSView subclass should raise a warning for methods in NSView that +// require localized strings +- (void)testRequiresLocalizationMethodFromSuperclass { + NSViewSubclass *s = [[NSViewSubclass alloc] init]; + NSString *bar = @"UnlocalizedString"; + + [s setToolTip:bar]; // expected-warning {{User-facing text should use localized string macro}} +} + +- (void)testRequiresLocalizationMethodFromProtocol { + UILabel *testLabel = [[UILabel alloc] init]; + + [testLabel setAccessibilityLabel:@"UnlocalizedString"]; // expected-warning {{User-facing text should use localized string macro}} +} + // EmptyLocalizationContextChecker tests #define HOM(s) YOLOC(s) #define YOLOC(x) NSLocalizedString(x, nil) Index: test/Analysis/localization.m =================================================================== --- test/Analysis/localization.m +++ test/Analysis/localization.m @@ -1,4 +1,4 @@ -// RUN: %clang_cc1 -analyze -fblocks -analyzer-store=region -analyzer-checker=alpha.osx.cocoa.NonLocalizedStringChecker -analyzer-checker=alpha.osx.cocoa.EmptyLocalizationContextChecker -verify %s +// RUN: %clang_cc1 -analyze -fblocks -analyzer-store=region -analyzer-checker=alpha.osx.cocoa.NonLocalizedStringChecker -analyzer-checker=alpha.osx.cocoa.PluralMisuseChecker -verify %s // The larger set of tests in located in localization.m. These are tests // specific for non-aggressive reporting. @@ -20,6 +20,8 @@ - (id)init; @end @interface NSString : NSObject +- (NSString *)stringByAppendingFormat:(NSString *)format, ...; ++ (instancetype)stringWithFormat:(NSString *)format, ...; @end @interface NSBundle : NSObject + (NSBundle *)mainBundle; @@ -36,11 +38,16 @@ @interface LocalizationTestSuite : NSObject int random(); +@property (assign) int unreadArticlesCount; @end - +#define MCLocalizedString(s) NSLocalizedString(s,nil); // Test cases begin here @implementation LocalizationTestSuite +NSString *KHLocalizedString(NSString* key, NSString* comment) { + return NSLocalizedString(key, comment); +} + // An object passed in as an parameter's string member // should not be considered unlocalized - (void)testObjectAsArgument:(TestObject *)argumentObject { @@ -58,7 +65,7 @@ bar = @"Unlocalized string"; } - [testLabel setText:bar]; // expected-warning {{String should be localized}} + [testLabel setText:bar]; // expected-warning {{User-facing text should use localized string macro}} } - (void)testOneCharacterStringsDoNotGiveAWarning { @@ -83,4 +90,75 @@ [testLabel setText:bar]; // no-warning } +// Plural Misuse Checker Tests + +- (NSString *)test1:(int)plural { + if (plural) { + return MCLocalizedString(@"TYPE_PLURAL"); // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}} + } + return MCLocalizedString(@"TYPE"); +} + +- (NSString *)test2:(int)numOfReminders { + if (numOfReminders > 0) { + return [NSString stringWithFormat:@"%@, %@", @"Test", (numOfReminders != 1) ? [NSString stringWithFormat:NSLocalizedString(@"%@ Reminders", @"Plural count of reminders"), numOfReminders] : [NSString stringWithFormat:NSLocalizedString(@"1 reminder", @"One reminder")]]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}} expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}} + } + return nil; +} + +- (void)test3 { + NSString *count; + if (self.unreadArticlesCount > 1) + { + count = [count stringByAppendingFormat:@"%@", KHLocalizedString(@"New Stories", @"Plural count for new stories")]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}} + } else { + count = [count stringByAppendingFormat:@"%@", KHLocalizedString(@"New Story", @"One new story")]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}} + } +} + +- (NSString *)test4:(int)count { + if ( count == 1 ) + { + return [NSString stringWithFormat:KHLocalizedString(@"value.singular",nil), count]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}} + } else { + return [NSString stringWithFormat:KHLocalizedString(@"value.plural",nil), count]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}} + } +} + +// Potential test-cases to support in the future + +// - (NSString *)test5:(int)count { +// BOOL plural = count != 1; +// return KHLocalizedString(plural ? @"PluralString" : @"SingularString", @""); +// } +// +// - (NSString *)test6:(BOOL)plural { +// return KHLocalizedString(([NSString stringWithFormat:@"RELATIVE_DATE_%@_%@", ((1 == 1) ? @"FUTURE" : @"PAST"), plural ? @"PLURAL" : @"SINGULAR"])); +// } +// +// +// +// - (void)test7:(int)numberOfTimesEarned { +// NSString* localizedDescriptionKey; +// if (numberOfTimesEarned == 1) { +// localizedDescriptionKey = @"SINGULAR_%@"; +// } else { +// localizedDescriptionKey = @"PLURAL_%@_%@"; +// } +// NSLocalizedString(localizedDescriptionKey, nil); +// } +// +// - (NSString *)test8 { +// NSInteger count = self.problems.count; +// NSString *title = [NSString stringWithFormat:@"%ld Problems", (long) count]; +// if (count < 2) { +// if (count == 0) { +// title = [NSString stringWithFormat:@"No Problems Found"]; +// } else { +// title = [NSString stringWithFormat:@"%ld Problem", (long) count]; +// } +// } +// return title; +// } + @end