800 changes: 703 additions & 97 deletions clang/lib/StaticAnalyzer/Checkers/LocalizationChecker.cpp

Large diffs are not rendered by default.

37 changes: 30 additions & 7 deletions clang/test/Analysis/localization-aggressive.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,21 @@ - (NSString *)localizedStringForKey:(NSString *)key
value:(NSString *)value
table:(NSString *)tableName;
@end
@interface UILabel : NSObject
@property(nullable, nonatomic, copy) NSString *text;
@protocol UIAccessibility
- (void)accessibilitySetIdentification:(NSString *)ident;
- (void)setAccessibilityLabel:(NSString *)label;
@end
@interface UILabel : NSObject <UIAccessibility>
@property(nullable, nonatomic, copy) NSString *text;
@end
@interface TestObject : NSObject
@property(strong) NSString *text;
@end
@interface NSView : NSObject
@property (strong) NSString *toolTip;
@end
@interface NSViewSubclass : NSView
@end

@interface LocalizationTestSuite : NSObject
NSString *ForceLocalized(NSString *str)
Expand Down Expand Up @@ -78,7 +86,7 @@ - (void)testLocalizationErrorDetectedOnPathway {
bar = @"Unlocalized string";
}

[testLabel setText:bar]; // expected-warning {{String should be localized}}
[testLabel setText:bar]; // expected-warning {{User-facing text should use localized string macro}}
}

- (void)testLocalizationErrorDetectedOnNSString {
Expand All @@ -88,7 +96,7 @@ - (void)testLocalizationErrorDetectedOnNSString {
bar = @"Unlocalized string";
}

[bar drawAtPoint:CGPointMake(0, 0) withAttributes:nil]; // expected-warning {{String should be localized}}
[bar drawAtPoint:CGPointMake(0, 0) withAttributes:nil]; // expected-warning {{User-facing text should use localized string macro}}
}

- (void)testNoLocalizationErrorDetectedFromCFunction {
Expand Down Expand Up @@ -137,7 +145,7 @@ - (void)testInlineEmptyStringLiteralHasLocalizedState {
// An string literal @"Hello" inline should raise an error
- (void)testInlineStringLiteralHasLocalizedState {
UILabel *testLabel = [[UILabel alloc] init];
[testLabel setText:@"Hello"]; // expected-warning {{String should be localized}}
[testLabel setText:@"Hello"]; // expected-warning {{User-facing text should use localized string macro}}
}

// A nil string should not raise an error
Expand Down Expand Up @@ -176,7 +184,7 @@ - (void)testLocalizedStringAsArgumentOtherMethod:(NSString *)argumentString {
- (void)localizedStringAsArgument:(NSString *)argumentString {
UILabel *testLabel = [[UILabel alloc] init];

[testLabel setText:argumentString]; // expected-warning {{String should be localized}}
[testLabel setText:argumentString]; // expected-warning {{User-facing text should use localized string macro}}
}

// [LocalizationTestSuite unLocalizedStringMethod] returns an unlocalized string
Expand All @@ -187,7 +195,7 @@ - (void)testUnLocalizedStringMethod {
UILabel *testLabel = [[UILabel alloc] init];
NSString *bar = NSLocalizedString(@"Hello", @"Comment");

[testLabel setText:[LocalizationTestSuite unLocalizedStringMethod]]; // expected-warning {{String should be localized}}
[testLabel setText:[LocalizationTestSuite unLocalizedStringMethod]]; // expected-warning {{User-facing text should use localized string macro}}
}

// This is the reverse situation: accessibilitySetIdentification: doesn't care
Expand All @@ -198,6 +206,21 @@ - (void)testMethodNotInRequiresLocalizedStringMethods {
[testLabel accessibilitySetIdentification:@"UnlocalizedString"]; // no-warning
}

// An NSView subclass should raise a warning for methods in NSView that
// require localized strings
- (void)testRequiresLocalizationMethodFromSuperclass {
NSViewSubclass *s = [[NSViewSubclass alloc] init];
NSString *bar = @"UnlocalizedString";

[s setToolTip:bar]; // expected-warning {{User-facing text should use localized string macro}}
}

- (void)testRequiresLocalizationMethodFromProtocol {
UILabel *testLabel = [[UILabel alloc] init];

[testLabel setAccessibilityLabel:@"UnlocalizedString"]; // expected-warning {{User-facing text should use localized string macro}}
}

// EmptyLocalizationContextChecker tests
#define HOM(s) YOLOC(s)
#define YOLOC(x) NSLocalizedString(x, nil)
Expand Down
127 changes: 124 additions & 3 deletions clang/test/Analysis/localization.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// RUN: %clang_cc1 -analyze -fblocks -analyzer-store=region -analyzer-checker=alpha.osx.cocoa.NonLocalizedStringChecker -analyzer-checker=alpha.osx.cocoa.EmptyLocalizationContextChecker -verify %s
// RUN: %clang_cc1 -analyze -fblocks -analyzer-store=region -analyzer-checker=alpha.osx.cocoa.NonLocalizedStringChecker -analyzer-checker=alpha.osx.cocoa.PluralMisuseChecker -verify %s

// The larger set of tests in located in localization.m. These are tests
// specific for non-aggressive reporting.
Expand All @@ -20,6 +20,8 @@ + (id)alloc;
- (id)init;
@end
@interface NSString : NSObject
- (NSString *)stringByAppendingFormat:(NSString *)format, ...;
+ (instancetype)stringWithFormat:(NSString *)format, ...;
@end
@interface NSBundle : NSObject
+ (NSBundle *)mainBundle;
Expand All @@ -36,11 +38,16 @@ @interface TestObject : NSObject

@interface LocalizationTestSuite : NSObject
int random();
@property (assign) int unreadArticlesCount;
@end

#define MCLocalizedString(s) NSLocalizedString(s,nil);
// Test cases begin here
@implementation LocalizationTestSuite

NSString *KHLocalizedString(NSString* key, NSString* comment) {
return NSLocalizedString(key, comment);
}

// An object passed in as an parameter's string member
// should not be considered unlocalized
- (void)testObjectAsArgument:(TestObject *)argumentObject {
Expand All @@ -58,7 +65,7 @@ - (void)testLocalizationErrorDetectedOnPathway {
bar = @"Unlocalized string";
}

[testLabel setText:bar]; // expected-warning {{String should be localized}}
[testLabel setText:bar]; // expected-warning {{User-facing text should use localized string macro}}
}

- (void)testOneCharacterStringsDoNotGiveAWarning {
Expand All @@ -83,4 +90,118 @@ - (void)testOneCharacterUTFStringsDoNotGiveAWarning {
[testLabel setText:bar]; // no-warning
}

// Plural Misuse Checker Tests
// These tests are modeled off incorrect uses of the many-one pattern
// from real projects.

- (NSString *)test1:(int)plural {
if (plural) {
return MCLocalizedString(@"TYPE_PLURAL"); // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
}
return MCLocalizedString(@"TYPE");
}

- (NSString *)test2:(int)numOfReminders {
if (numOfReminders > 0) {
return [NSString stringWithFormat:@"%@, %@", @"Test", (numOfReminders != 1) ? [NSString stringWithFormat:NSLocalizedString(@"%@ Reminders", @"Plural count of reminders"), numOfReminders] : [NSString stringWithFormat:NSLocalizedString(@"1 reminder", @"One reminder")]]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}} expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
}
return nil;
}

- (void)test3 {
NSString *count;
if (self.unreadArticlesCount > 1)
{
count = [count stringByAppendingFormat:@"%@", KHLocalizedString(@"New Stories", @"Plural count for new stories")]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
} else {
count = [count stringByAppendingFormat:@"%@", KHLocalizedString(@"New Story", @"One new story")]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
}
}

- (NSString *)test4:(int)count {
if ( count == 1 )
{
return [NSString stringWithFormat:KHLocalizedString(@"value.singular",nil), count]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
} else {
return [NSString stringWithFormat:KHLocalizedString(@"value.plural",nil), count]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
}
}

- (NSString *)test5:(int)count {
int test = count == 1;
if (test)
{
return [NSString stringWithFormat:KHLocalizedString(@"value.singular",nil), count]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
} else {
return [NSString stringWithFormat:KHLocalizedString(@"value.plural",nil), count]; // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
}
}

// This tests the heuristic that the direct parent IfStmt must match the isCheckingPlurality confition to avoid false positives generated from complex code (generally the pattern we're looking for is simple If-Else)

- (NSString *)test6:(int)sectionIndex {
int someOtherVariable = 0;
if (sectionIndex == 1)
{
// Do some other crazy stuff
if (someOtherVariable)
return KHLocalizedString(@"OK",nil); // no-warning
} else {
return KHLocalizedString(@"value.plural",nil); // expected-warning {{Plural cases are not supported accross all languages. Use a .stringsdict file}}
}
return nil;
}

// False positives that we are not accounting for involve matching the heuristic
// of having 1 or 2 in the RHS of a BinaryOperator and having a localized string
// in the body of the IfStmt. This is seen a lot when checking for the section
// indexpath of something like a UITableView

// - (NSString *)testNotAccountedFor:(int)sectionIndex {
// if (sectionIndex == 1)
// {
// return KHLocalizedString(@"1",nil); // false-positive
// } else if (sectionIndex == 2) {
// return KHLocalizedString(@"2",nil); // false-positive
// } else if (sectionIndex == 3) {
// return KHLocalizedString(@"3",nil); // no-false-positive
// }
// }

// Potential test-cases to support in the future

// - (NSString *)test7:(int)count {
// BOOL plural = count != 1;
// return KHLocalizedString(plural ? @"PluralString" : @"SingularString", @"");
// }
//
// - (NSString *)test8:(BOOL)plural {
// return KHLocalizedString(([NSString stringWithFormat:@"RELATIVE_DATE_%@_%@", ((1 == 1) ? @"FUTURE" : @"PAST"), plural ? @"PLURAL" : @"SINGULAR"]));
// }
//
//
//
// - (void)test9:(int)numberOfTimesEarned {
// NSString* localizedDescriptionKey;
// if (numberOfTimesEarned == 1) {
// localizedDescriptionKey = @"SINGULAR_%@";
// } else {
// localizedDescriptionKey = @"PLURAL_%@_%@";
// }
// NSLocalizedString(localizedDescriptionKey, nil);
// }
//
// - (NSString *)test10 {
// NSInteger count = self.problems.count;
// NSString *title = [NSString stringWithFormat:@"%ld Problems", (long) count];
// if (count < 2) {
// if (count == 0) {
// title = [NSString stringWithFormat:@"No Problems Found"];
// } else {
// title = [NSString stringWithFormat:@"%ld Problem", (long) count];
// }
// }
// return title;
// }

@end