Streamline JSRetainPtr, fix leaks of JSString and JSGlobalContext
[WebKit-https.git] / Tools / TestRunnerShared / cocoa / LayoutTestSpellChecker.mm
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "LayoutTestSpellChecker.h"
28
29 #import <JavaScriptCore/JSRetainPtr.h>
30 #import <objc/runtime.h>
31 #import <wtf/Assertions.h>
32 #import <wtf/BlockPtr.h>
33
34 #if PLATFORM(MAC)
35
36 using TextCheckingCompletionHandler = void(^)(NSInteger, NSArray<NSTextCheckingResult *> *, NSOrthography *, NSInteger);
37
38 static LayoutTestSpellChecker *globalSpellChecker = nil;
39 static BOOL hasSwizzledLayoutTestSpellChecker = NO;
40 static IMP globallySwizzledSharedSpellCheckerImplementation;
41 static Method originalSharedSpellCheckerMethod;
42
43 static LayoutTestSpellChecker *ensureGlobalLayoutTestSpellChecker()
44 {
45     static dispatch_once_t onceToken;
46     dispatch_once(&onceToken, ^{
47         globalSpellChecker = [[LayoutTestSpellChecker alloc] init];
48     });
49     return globalSpellChecker;
50 }
51
52 static const char *stringForCorrectionResponse(NSCorrectionResponse correctionResponse)
53 {
54     switch (correctionResponse) {
55     case NSCorrectionResponseNone:
56         return "none";
57     case NSCorrectionResponseAccepted:
58         return "accepted";
59     case NSCorrectionResponseRejected:
60         return "rejected";
61     case NSCorrectionResponseIgnored:
62         return "ignored";
63     case NSCorrectionResponseEdited:
64         return "edited";
65     case NSCorrectionResponseReverted:
66         return "reverted";
67     }
68     return "invalid";
69 }
70
71 static NSTextCheckingType nsTextCheckingType(JSStringRef jsType)
72 {
73     auto cfType = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, jsType));
74     if (CFStringCompare(cfType.get(), CFSTR("orthography"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
75         return NSTextCheckingTypeOrthography;
76
77     if (CFStringCompare(cfType.get(), CFSTR("spelling"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
78         return NSTextCheckingTypeSpelling;
79
80     if (CFStringCompare(cfType.get(), CFSTR("grammar"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
81         return NSTextCheckingTypeGrammar;
82
83     if (CFStringCompare(cfType.get(), CFSTR("date"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
84         return NSTextCheckingTypeDate;
85
86     if (CFStringCompare(cfType.get(), CFSTR("address"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
87         return NSTextCheckingTypeAddress;
88
89     if (CFStringCompare(cfType.get(), CFSTR("link"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
90         return NSTextCheckingTypeLink;
91
92     if (CFStringCompare(cfType.get(), CFSTR("quote"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
93         return NSTextCheckingTypeQuote;
94
95     if (CFStringCompare(cfType.get(), CFSTR("dash"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
96         return NSTextCheckingTypeDash;
97
98     if (CFStringCompare(cfType.get(), CFSTR("replacement"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
99         return NSTextCheckingTypeReplacement;
100
101     if (CFStringCompare(cfType.get(), CFSTR("correction"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
102         return NSTextCheckingTypeCorrection;
103
104     if (CFStringCompare(cfType.get(), CFSTR("regular-expression"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
105         return NSTextCheckingTypeRegularExpression;
106
107     if (CFStringCompare(cfType.get(), CFSTR("phone-number"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
108         return NSTextCheckingTypePhoneNumber;
109
110     if (CFStringCompare(cfType.get(), CFSTR("transit-information"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
111         return NSTextCheckingTypeTransitInformation;
112
113     ASSERT_NOT_REACHED();
114     return NSTextCheckingTypeSpelling;
115 }
116
117 @interface LayoutTestTextCheckingResult : NSTextCheckingResult {
118 @private
119     RetainPtr<NSString> _replacement;
120     NSTextCheckingType _type;
121     NSRange _range;
122     RetainPtr<NSArray<NSDictionary *>> _details;
123 }
124
125 - (instancetype)initWithType:(NSTextCheckingType)type range:(NSRange)range replacement:(NSString *)replacement details:(NSArray<NSDictionary<NSString *, id> *> *)details;
126 @end
127
128 @implementation LayoutTestTextCheckingResult
129
130 - (instancetype)initWithType:(NSTextCheckingType)type range:(NSRange)range replacement:(NSString *)replacement details:(NSArray<NSDictionary<NSString *, id> *> *)details
131 {
132     if (!(self = [super init]))
133         return nil;
134
135     _type = type;
136     _range = range;
137     _replacement = adoptNS(replacement.copy);
138     _details = adoptNS(details.copy);
139
140     return self;
141 }
142
143 - (NSArray<NSDictionary<NSString *, id> *> *)grammarDetails
144 {
145     return _details.get();
146 }
147
148 - (NSRange)range
149 {
150     return _range;
151 }
152
153 - (NSTextCheckingType)resultType
154 {
155     return _type;
156 }
157
158 - (NSString *)replacementString
159 {
160     return _replacement.get();
161 }
162
163 - (NSString *)description
164 {
165     return [NSString stringWithFormat:@"<%@ %p type=%tu range=[%tu, %tu] replacement='%@'>", self.class, self, _type, _range.location, _range.location + _range.length, _replacement.get()];
166 }
167
168 @end
169
170 @implementation LayoutTestSpellChecker
171
172 @synthesize spellCheckerLoggingEnabled=_spellCheckerLoggingEnabled;
173
174 + (instancetype)checker
175 {
176     auto *spellChecker = ensureGlobalLayoutTestSpellChecker();
177     if (hasSwizzledLayoutTestSpellChecker)
178         return spellChecker;
179
180     originalSharedSpellCheckerMethod = class_getClassMethod(objc_getMetaClass("NSSpellChecker"), @selector(sharedSpellChecker));
181     globallySwizzledSharedSpellCheckerImplementation = method_setImplementation(originalSharedSpellCheckerMethod, reinterpret_cast<IMP>(ensureGlobalLayoutTestSpellChecker));
182     hasSwizzledLayoutTestSpellChecker = YES;
183     return spellChecker;
184 }
185
186 + (void)uninstallAndReset
187 {
188     [globalSpellChecker reset];
189     if (!hasSwizzledLayoutTestSpellChecker)
190         return;
191
192     method_setImplementation(originalSharedSpellCheckerMethod, globallySwizzledSharedSpellCheckerImplementation);
193     hasSwizzledLayoutTestSpellChecker = NO;
194 }
195
196 - (void)reset
197 {
198     self.results = nil;
199     self.spellCheckerLoggingEnabled = NO;
200 }
201
202 - (TextCheckingResultsDictionary *)results
203 {
204     return _results.get();
205 }
206
207 - (void)setResults:(TextCheckingResultsDictionary *)results
208 {
209     _results = adoptNS(results.copy);
210 }
211
212 - (void)setResultsFromJSObject:(JSObjectRef)resultsObject inContext:(JSContextRef)context
213 {
214     auto fromPropertyName = adopt(JSStringCreateWithUTF8CString("from"));
215     auto toPropertyName = adopt(JSStringCreateWithUTF8CString("to"));
216     auto typePropertyName = adopt(JSStringCreateWithUTF8CString("type"));
217     auto replacementPropertyName = adopt(JSStringCreateWithUTF8CString("replacement"));
218     auto detailsPropertyName = adopt(JSStringCreateWithUTF8CString("details"));
219     auto results = adoptNS([[NSMutableDictionary alloc] init]);
220
221     // FIXME: Using the Objective-C API would make this logic easier to follow.
222     auto properties = JSObjectCopyPropertyNames(context, resultsObject);
223     for (size_t index = 0; index < JSPropertyNameArrayGetCount(properties); ++index) {
224         JSStringRef textToCheck = JSPropertyNameArrayGetNameAtIndex(properties, index);
225         JSObjectRef resultsArray = JSValueToObject(context, JSObjectGetProperty(context, resultsObject, textToCheck, nullptr), nullptr);
226         auto resultsArrayPropertyNames = JSObjectCopyPropertyNames(context, resultsArray);
227         auto resultsForWord = adoptNS([[NSMutableArray alloc] init]);
228         for (size_t resultIndex = 0; resultIndex < JSPropertyNameArrayGetCount(resultsArrayPropertyNames); ++resultIndex) {
229             auto resultsObject = JSValueToObject(context, JSObjectGetPropertyAtIndex(context, resultsArray, resultIndex, nullptr), nullptr);
230             long fromValue = lroundl(JSValueToNumber(context, JSObjectGetProperty(context, resultsObject, fromPropertyName.get(), nullptr), nullptr));
231             long toValue = lroundl(JSValueToNumber(context, JSObjectGetProperty(context, resultsObject, toPropertyName.get(), nullptr), nullptr));
232             auto typeValue = adopt(JSValueToStringCopy(context, JSObjectGetProperty(context, resultsObject, typePropertyName.get(), nullptr), nullptr));
233             auto replacementValue = JSObjectGetProperty(context, resultsObject, replacementPropertyName.get(), nullptr);
234             RetainPtr<CFStringRef> replacementText;
235             if (!JSValueIsUndefined(context, replacementValue)) {
236                 auto replacementJSString = adopt(JSValueToStringCopy(context, replacementValue, nullptr));
237                 replacementText = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, replacementJSString.get()));
238             }
239             auto details = adoptNS([[NSMutableArray alloc] init]);
240             auto detailsValue = JSObjectGetProperty(context, resultsObject, detailsPropertyName.get(), nullptr);
241             if (!JSValueIsUndefined(context, detailsValue)) {
242                 auto detailsObject = JSValueToObject(context, detailsValue, nullptr);
243                 auto detailsObjectProperties = JSObjectCopyPropertyNames(context, detailsObject);
244                 for (size_t detailIndex = 0; detailIndex < JSPropertyNameArrayGetCount(detailsObjectProperties); ++detailIndex) {
245                     auto detail = adoptNS([[NSMutableDictionary alloc] init]);
246                     auto detailObject = JSValueToObject(context, JSObjectGetPropertyAtIndex(context, detailsObject, detailIndex, nullptr), nullptr);
247                     long from = lroundl(JSValueToNumber(context, JSObjectGetProperty(context, detailObject, fromPropertyName.get(), nullptr), nullptr));
248                     long to = lroundl(JSValueToNumber(context, JSObjectGetProperty(context, detailObject, toPropertyName.get(), nullptr), nullptr));
249                     [detail setObject:[NSValue valueWithRange:NSMakeRange(from, to - from)] forKey:NSGrammarRange];
250                     [details addObject:detail.get()];
251                 }
252                 JSPropertyNameArrayRelease(detailsObjectProperties);
253             }
254             [resultsForWord addObject:[[[LayoutTestTextCheckingResult alloc] initWithType:nsTextCheckingType(typeValue.get()) range:NSMakeRange(fromValue, toValue - fromValue) replacement:(__bridge NSString *)replacementText.get() details:details.get()] autorelease]];
255         }
256         auto cfTextToCheck = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, textToCheck));
257         [results setObject:resultsForWord.get() forKey:(__bridge NSString *)cfTextToCheck.get()];
258         JSPropertyNameArrayRelease(resultsArrayPropertyNames);
259     }
260     JSPropertyNameArrayRelease(properties);
261
262     _results = WTFMove(results);
263 }
264
265 - (NSArray<NSTextCheckingResult *> *)checkString:(NSString *)stringToCheck range:(NSRange)range types:(NSTextCheckingTypes)checkingTypes options:(NSDictionary<NSString *, id> *)options inSpellDocumentWithTag:(NSInteger)tag orthography:(NSOrthography **)orthography wordCount:(NSInteger *)wordCount
266 {
267     NSArray *result = [super checkString:stringToCheck range:range types:checkingTypes options:options inSpellDocumentWithTag:tag orthography:orthography wordCount:wordCount];
268     if (auto *overrideResult = [_results objectForKey:stringToCheck])
269         return overrideResult;
270
271     return result;
272 }
273
274 - (void)recordResponse:(NSCorrectionResponse)response toCorrection:(NSString *)correction forWord:(NSString *)word language:(NSString *)language inSpellDocumentWithTag:(NSInteger)tag
275 {
276     if (_spellCheckerLoggingEnabled)
277         printf("NSSpellChecker recordResponseToCorrection: %s -> %s (response: %s)\n", [word UTF8String], [correction UTF8String], stringForCorrectionResponse(response));
278
279     [super recordResponse:response toCorrection:correction forWord:word language:language inSpellDocumentWithTag:tag];
280 }
281
282 - (NSInteger)requestCheckingOfString:(NSString *)stringToCheck range:(NSRange)range types:(NSTextCheckingTypes)checkingTypes options:(NSDictionary<NSString *, id> *)options inSpellDocumentWithTag:(NSInteger)tag completionHandler:(TextCheckingCompletionHandler)completionHandler
283 {
284     return [super requestCheckingOfString:stringToCheck range:range types:checkingTypes options:options inSpellDocumentWithTag:tag completionHandler:[overrideResult = retainPtr([_results objectForKey:stringToCheck]), completion = makeBlockPtr(completionHandler), stringToCheck = retainPtr(stringToCheck)] (NSInteger sequenceNumber, NSArray<NSTextCheckingResult *> *result, NSOrthography *orthography, NSInteger wordCount) {
285         if (overrideResult) {
286             completion(sequenceNumber, overrideResult.get(), orthography, wordCount);
287             return;
288         }
289
290         completion(sequenceNumber, result, orthography, wordCount);
291     }];
292 }
293
294 @end
295
296 #endif // PLATFORM(MAC)