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