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