From 35739c9923c394757e2d3bfc094b1f14b6530f6b Mon Sep 17 00:00:00 2001 From: Anders Hasselqvist Date: Thu, 17 Jan 2019 16:11:13 +0900 Subject: [PATCH] Implement NSTextCheckingResult.range(withName:) - Added the missing range(withName:) and corresponding CF function. The implementation relies on uregex_groupNumberFromName which is available as a draft API from ICU 55. - Added tests related to named capture groups. - Add availability checks in the tests to run with DarwinCompatibilityTests. --- .../String.subproj/CFRegularExpression.c | 35 ++++++++++ .../String.subproj/CFRegularExpression.h | 1 + Foundation/NSRegularExpression.swift | 6 +- Foundation/NSTextCheckingResult.swift | 11 ++++ TestFoundation/TestNSRegularExpression.swift | 27 +++++++- TestFoundation/TestNSTextCheckingResult.swift | 64 ++++++++++++++++++- 6 files changed, 141 insertions(+), 3 deletions(-) diff --git a/CoreFoundation/String.subproj/CFRegularExpression.c b/CoreFoundation/String.subproj/CFRegularExpression.c index d6340f0e41..6c38c6ccce 100644 --- a/CoreFoundation/String.subproj/CFRegularExpression.c +++ b/CoreFoundation/String.subproj/CFRegularExpression.c @@ -171,6 +171,41 @@ CFIndex _CFRegularExpressionGetNumberOfCaptureGroups(_CFRegularExpressionRef reg return (CFIndex)uregex_groupCount(regex->regex, &errorCode); } +CFIndex _CFRegularExpressionGetCaptureGroupNumberWithName(_CFRegularExpressionRef regex, CFStringRef groupName) { + UniChar stackBuffer[STACK_BUFFER_SIZE], *nameBuffer = NULL; + Boolean freeNameBuffer = false; + + CFIndex nameLength = CFStringGetLength(groupName); + UErrorCode errorCode = U_ZERO_ERROR; + + nameBuffer = (UniChar *)CFStringGetCharactersPtr(groupName); + if (!nameBuffer) { + if (nameLength <= STACK_BUFFER_SIZE) { + nameBuffer = stackBuffer; + CFStringGetCharacters(groupName, CFRangeMake(0, nameLength), nameBuffer); + } else { + nameBuffer = (UniChar *)malloc(sizeof(UniChar) * nameLength); + if (nameBuffer) { + CFStringGetCharacters(groupName, CFRangeMake(0, nameLength), nameBuffer); + freeNameBuffer = true; + } else { + HALT; + } + } + } + + CFIndex idx = uregex_groupNumberFromName(regex->regex, nameBuffer, nameLength, &errorCode); + if (U_FAILURE(errorCode) || idx < 0) { + idx = kCFNotFound; + } + + if (freeNameBuffer) { + free(nameBuffer); + } + + return idx; +} + struct regexCallBackContext { void *context; void (*match)(void *context, CFRange *ranges, CFIndex count, _CFRegularExpressionMatchingFlags flags, Boolean *stop); diff --git a/CoreFoundation/String.subproj/CFRegularExpression.h b/CoreFoundation/String.subproj/CFRegularExpression.h index 583ce4e6d6..32768f4e9d 100644 --- a/CoreFoundation/String.subproj/CFRegularExpression.h +++ b/CoreFoundation/String.subproj/CFRegularExpression.h @@ -57,6 +57,7 @@ _CFRegularExpressionRef _Nullable _CFRegularExpressionCreate(CFAllocatorRef allo void _CFRegularExpressionDestroy(_CFRegularExpressionRef regex); CFIndex _CFRegularExpressionGetNumberOfCaptureGroups(_CFRegularExpressionRef regex); +CFIndex _CFRegularExpressionGetCaptureGroupNumberWithName(_CFRegularExpressionRef regex, CFStringRef groupName); void _CFRegularExpressionEnumerateMatchesInString(_CFRegularExpressionRef regexObj, CFStringRef string, _CFRegularExpressionMatchingOptions options, CFRange range, void *_Nullable context, _CFRegularExpressionMatch match); CFStringRef _CFRegularExpressionGetPattern(_CFRegularExpressionRef regex); diff --git a/Foundation/NSRegularExpression.swift b/Foundation/NSRegularExpression.swift index ae3d650207..4d81770769 100644 --- a/Foundation/NSRegularExpression.swift +++ b/Foundation/NSRegularExpression.swift @@ -106,7 +106,11 @@ open class NSRegularExpression: NSObject, NSCopying, NSCoding { open var numberOfCaptureGroups: Int { return _CFRegularExpressionGetNumberOfCaptureGroups(_internal) } - + + internal func _captureGroupNumber(withName name: String) -> Int { + return _CFRegularExpressionGetCaptureGroupNumberWithName(_internal, name._cfObject) + } + /* This class method will produce a string by adding backslash escapes as necessary to the given string, to escape any characters that would otherwise be treated as pattern metacharacters. */ open class func escapedPattern(for string: String) -> String { diff --git a/Foundation/NSTextCheckingResult.swift b/Foundation/NSTextCheckingResult.swift index 74dd8c0a1c..07de81337c 100644 --- a/Foundation/NSTextCheckingResult.swift +++ b/Foundation/NSTextCheckingResult.swift @@ -54,6 +54,8 @@ open class NSTextCheckingResult: NSObject, NSCopying, NSCoding { open var range: NSRange { return range(at: 0) } /* A result must have at least one range, but may optionally have more (for example, to represent regular expression capture groups). The range at index 0 always matches the range property. Additional ranges, if any, will have indexes from 1 to numberOfRanges-1. */ open func range(at idx: Int) -> NSRange { NSRequiresConcreteImplementation() } + @available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) + open func range(withName: String) -> NSRange { NSRequiresConcreteImplementation() } open var regularExpression: NSRegularExpression? { return nil } open var numberOfRanges: Int { return 1 } } @@ -81,6 +83,15 @@ internal class _NSRegularExpressionNSTextCheckingResultResult : NSTextCheckingRe override var resultType: CheckingType { return .RegularExpression } override func range(at idx: Int) -> NSRange { return _ranges[idx] } + override func range(withName name: String) -> NSRange { + let idx = _regularExpression._captureGroupNumber(withName: name) + if idx != kCFNotFound, idx < numberOfRanges { + return range(at: idx) + } + + return NSRange(location: NSNotFound, length: 0) + } + override var numberOfRanges: Int { return _ranges.count } override var regularExpression: NSRegularExpression? { return _regularExpression } } diff --git a/TestFoundation/TestNSRegularExpression.swift b/TestFoundation/TestNSRegularExpression.swift index 419ba3b3ec..c3b8a4bcea 100644 --- a/TestFoundation/TestNSRegularExpression.swift +++ b/TestFoundation/TestNSRegularExpression.swift @@ -18,9 +18,11 @@ class TestNSRegularExpression : XCTestCase { ("test_NSCoding", test_NSCoding), ("test_defaultOptions", test_defaultOptions), ("test_badPattern", test_badPattern), + ("test_unicodeNamedGroup", test_unicodeNamedGroup), + ("test_conflictingNamedGroups", test_conflictingNamedGroups), ] } - + func simpleRegularExpressionTestWithPattern(_ patternString: String, target searchString: String, looking: Bool, match: Bool, file: StaticString = #file, line: UInt = #line) { do { let str = NSString(string: searchString) @@ -371,4 +373,27 @@ class TestNSRegularExpression : XCTestCase { XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(}") } } + + func test_unicodeNamedGroup() { + let patternString = "(?<りんご>a)" + do { + _ = try NSRegularExpression(pattern: patternString, options: []) + XCTFail("Building regular expression for pattern with unicode group name should fail.") + } catch { + let err = String(describing: error) + XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(?<りんご>a)}") + } + } + + func test_conflictingNamedGroups() { + let patternString = "(?a)(?b)" + do { + _ = try NSRegularExpression(pattern: patternString, options: []) + XCTFail("Building regular expression for pattern with identically named groups should fail.") + } catch { + let err = String(describing: error) + XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(?a)(?b)}") + } + } + } diff --git a/TestFoundation/TestNSTextCheckingResult.swift b/TestFoundation/TestNSTextCheckingResult.swift index 303045577b..16c3f01ced 100644 --- a/TestFoundation/TestNSTextCheckingResult.swift +++ b/TestFoundation/TestNSTextCheckingResult.swift @@ -11,11 +11,13 @@ class TestNSTextCheckingResult: XCTestCase { static var allTests: [(String, (TestNSTextCheckingResult) -> () throws -> Void)] { return [ ("test_textCheckingResult", test_textCheckingResult), + ("test_multipleMatches", test_multipleMatches), + ("test_rangeWithName", test_rangeWithName), ] } func test_textCheckingResult() { - let patternString = "(a|b)x|123|(c|d)y" + let patternString = "(a|b)x|123|(?c|d)y" do { let patternOptions: NSRegularExpression.Options = [] let regex = try NSRegularExpression(pattern: patternString, options: patternOptions) @@ -28,16 +30,76 @@ class TestNSTextCheckingResult: XCTestCase { XCTAssertEqual(result.range(at: 0).location, 6) XCTAssertEqual(result.range(at: 1).location, NSNotFound) XCTAssertEqual(result.range(at: 2).location, 6) + if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { + XCTAssertEqual(result.range(withName: "aname").location, 6) + } //Negative offset result = match.adjustingRanges(offset: -2) XCTAssertEqual(result.range(at: 0).location, 3) XCTAssertEqual(result.range(at: 1).location, NSNotFound) XCTAssertEqual(result.range(at: 2).location, 3) + if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { + XCTAssertEqual(result.range(withName: "aname").location, 3) + } //ZeroOffset result = match.adjustingRanges(offset: 0) XCTAssertEqual(result.range(at: 0).location, 5) XCTAssertEqual(result.range(at: 1).location, NSNotFound) XCTAssertEqual(result.range(at: 2).location, 5) + if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { + XCTAssertEqual(result.range(withName: "aname").location, 5) + } + } catch { + XCTFail("Unable to build regular expression for pattern \(patternString)") + } + } + + func test_multipleMatches() { + let patternString = "(?hello)[0-9]" + + do { + let regex = try NSRegularExpression(pattern: patternString, options: []) + let searchString = "hello1 hello2" + let searchRange = NSRange(location: 0, length: searchString.count) + let matches = regex.matches(in: searchString, options: [], range: searchRange) + XCTAssertEqual(matches.count, 2) + XCTAssertEqual(matches[0].numberOfRanges, 2) + XCTAssertEqual(matches[0].range, NSRange(location: 0, length: 6)) + XCTAssertEqual(matches[0].range(at: 0), NSRange(location: 0, length: 6)) + XCTAssertEqual(matches[0].range(at: 1), NSRange(location: 0, length: 5)) + if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { + XCTAssertEqual(matches[0].range(withName: "name"), NSRange(location: 0, length: 5)) + } + XCTAssertEqual(matches[1].numberOfRanges, 2) + XCTAssertEqual(matches[1].range, NSRange(location: 7, length: 6)) + XCTAssertEqual(matches[1].range(at: 0), NSRange(location: 7, length: 6)) + XCTAssertEqual(matches[1].range(at: 1), NSRange(location: 7, length: 5)) + if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { + XCTAssertEqual(matches[1].range(withName: "name"), NSRange(location: 7, length: 5)) + } + } catch { + XCTFail("Unable to build regular expression for pattern \(patternString)") + } + } + + + func test_rangeWithName() { + guard #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) else { + return + } + + let patternString = "(?hel)lo, (?worl)d" + + do { + let regex = try NSRegularExpression(pattern: patternString, options: []) + let searchString = "hello, world" + let searchRange = NSRange(location: 0, length: searchString.count) + let matches = regex.matches(in: searchString, options: [], range: searchRange) + XCTAssertEqual(matches.count, 1) + XCTAssertEqual(matches[0].numberOfRanges, 3) + XCTAssertEqual(matches[0].range(withName: "incorrect").location, NSNotFound) + XCTAssertEqual(matches[0].range(withName: "name1"), NSRange(location: 0, length: 3)) + XCTAssertEqual(matches[0].range(withName: "name2"), NSRange(location: 7, length: 4)) } catch { XCTFail("Unable to build regular expression for pattern \(patternString)") }