Skip to content

Commit 094c87f

Browse files
authored
Merge pull request #2 from regexident/improve-init
Improvements
2 parents a6a74a2 + 7dd4ca5 commit 094c87f

5 files changed

Lines changed: 382 additions & 268 deletions

File tree

Sources/Pathspec/GitIgnoreSpec.swift

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,25 @@
88
import Foundation
99

1010
struct GitIgnoreSpec: Spec {
11+
enum Error: Swift.Error {
12+
case emptyPattern
13+
case commented
14+
case invalid
15+
case emptyRoot
16+
}
17+
1118
private(set) var inclusive: Bool = true
19+
20+
let pattern: String
1221
let regex: NSRegularExpression
13-
14-
init?(pattern: String) {
15-
guard !pattern.isEmpty else { return nil }
16-
guard !pattern.hasPrefix("#") else { return nil }
17-
guard !pattern.contains("***") else { return nil }
18-
guard pattern != "/" else { return nil }
22+
23+
init(pattern: String) throws {
24+
self.pattern = pattern
25+
26+
guard !pattern.isEmpty else { throw Error.emptyPattern }
27+
guard !pattern.hasPrefix("#") else { throw Error.commented }
28+
guard !pattern.contains("***") else { throw Error.invalid }
29+
guard pattern != "/" else { throw Error.emptyRoot }
1930

2031
var pattern = pattern
2132
if pattern.hasPrefix("!") {
@@ -81,11 +92,7 @@ struct GitIgnoreSpec: Spec {
8192

8293
regexString += "$"
8394

84-
do {
85-
regex = try NSRegularExpression(pattern: regexString, options: [])
86-
} catch {
87-
return nil
88-
}
95+
regex = try NSRegularExpression(pattern: regexString, options: [])
8996
}
9097

9198
func match(file: String) -> Bool {
@@ -152,3 +159,18 @@ struct GitIgnoreSpec: Spec {
152159
return regex
153160
}
154161
}
162+
163+
extension GitIgnoreSpec: CustomStringConvertible {
164+
var description: String {
165+
let pattern = self.pattern.debugDescription
166+
return "<\(type(of: self)) pattern: \(pattern)>"
167+
}
168+
}
169+
170+
extension GitIgnoreSpec: CustomDebugStringConvertible {
171+
var debugDescription: String {
172+
let pattern = self.pattern.debugDescription
173+
let regexPattern = self.regex.pattern.debugDescription
174+
return "<\(type(of: self)) pattern: \(pattern) regex: \(regexPattern)>"
175+
}
176+
}

Sources/Pathspec/Pathspec.swift

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
public final class Pathspec {
99
private let specs: [Spec]
1010

11-
public init(patterns: String...) {
12-
specs = patterns.compactMap {
13-
GitIgnoreSpec(pattern: $0)
14-
}
11+
public convenience init(patterns: [String]) throws {
12+
self.init(specs: try patterns.map {
13+
try GitIgnoreSpec(pattern: $0)
14+
})
1515
}
16+
17+
public init(specs: [Spec]) {
18+
self.specs = specs
19+
}
1620

1721
public func match(path: String) -> Bool {
1822
let matchingSpecs = self.matchingSpecs(path: path)
@@ -24,3 +28,31 @@ public final class Pathspec {
2428
return specs.filter { $0.match(file: path) }
2529
}
2630
}
31+
32+
extension Pathspec: ExpressibleByArrayLiteral {
33+
public typealias ArrayLiteralElement = String
34+
35+
public convenience init(arrayLiteral: String...) {
36+
self.init(specs: arrayLiteral.compactMap {
37+
try? GitIgnoreSpec(pattern: $0)
38+
})
39+
}
40+
}
41+
42+
extension Pathspec: CustomStringConvertible {
43+
public var description: String {
44+
let specsDescription = specs.map { spec in
45+
" " + String(describing: spec)
46+
}.joined(separator: ",\n")
47+
return "<\(type(of: self)) specs: [\n\(specsDescription)\n]>"
48+
}
49+
}
50+
51+
extension Pathspec: CustomDebugStringConvertible {
52+
public var debugDescription: String {
53+
let specsDescription = specs.map { spec in
54+
" " + String(reflecting: spec)
55+
}.joined(separator: ",\n")
56+
return "<\(type(of: self)) specs: [\n\(specsDescription)\n]>"
57+
}
58+
}
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
//
2+
// PathspecTests.swift
3+
// Pathspec
4+
//
5+
// Created by Geoffrey Foster on 2019-06-29.
6+
//
7+
8+
import XCTest
9+
@testable import Pathspec
10+
11+
final class GitIgnoreSpecTests: XCTestCase {
12+
func testDescription() throws {
13+
let spec = try XCTUnwrap(GitIgnoreSpec(pattern: "foobar"))
14+
15+
XCTAssertEqual(spec.description, "<GitIgnoreSpec pattern: \"foobar\">")
16+
}
17+
18+
func testDebugDescription() throws {
19+
let spec = try XCTUnwrap(GitIgnoreSpec(pattern: "foobar"))
20+
21+
XCTAssertEqual(spec.debugDescription, "<GitIgnoreSpec pattern: \"foobar\" regex: \"^(?:.+/)?foobar(?:/.*)?$\">")
22+
}
23+
24+
func testAbsoluteRoot() throws {
25+
XCTAssertThrowsError(try GitIgnoreSpec(pattern: "/"))
26+
}
27+
28+
func testComment() throws {
29+
XCTAssertThrowsError(try GitIgnoreSpec(pattern: "# Cork soakers."))
30+
}
31+
32+
func testIgnore() throws {
33+
let spec = try XCTUnwrap(GitIgnoreSpec(pattern: "!temp"))
34+
XCTAssertFalse(spec.inclusive)
35+
XCTAssertEqual(spec.regex.pattern, "^(?:.+/)?temp$")
36+
let result = spec.match(file: "temp/foo")
37+
XCTAssertEqual(result, false)
38+
}
39+
40+
// MARK: - Inclusive tests
41+
42+
@inline(__always)
43+
private func _testRunner(pattern: String, regex: String, files: [String], expectedResults: [String], file: StaticString = #file, line: UInt = #line) throws {
44+
let spec = try XCTUnwrap(GitIgnoreSpec(pattern: pattern), file: file, line: line)
45+
XCTAssertTrue(spec.inclusive, file: file, line: line)
46+
XCTAssertEqual(spec.regex.pattern, regex, file: file, line: line)
47+
let results = spec.match(files: files)
48+
XCTAssertEqual(results, expectedResults, file: file, line: line)
49+
}
50+
51+
func testAbsolute() throws {
52+
try _testRunner(
53+
pattern: "/an/absolute/file/path",
54+
regex: "^an/absolute/file/path(?:/.*)?$",
55+
files: [
56+
"an/absolute/file/path",
57+
"an/absolute/file/path/foo",
58+
"foo/an/absolute/file/path",
59+
],
60+
expectedResults: [
61+
"an/absolute/file/path",
62+
"an/absolute/file/path/foo",
63+
]
64+
)
65+
}
66+
67+
func testAbsoluteSingleItem() throws {
68+
try _testRunner(
69+
pattern: "/an/",
70+
regex: "^an/.*$",
71+
files: [
72+
"an/absolute/file/path",
73+
"an/absolute/file/path/foo",
74+
"foo/an/absolute/file/path",
75+
],
76+
expectedResults: [
77+
"an/absolute/file/path",
78+
"an/absolute/file/path/foo",
79+
]
80+
)
81+
}
82+
83+
func testRelative() throws {
84+
try _testRunner(
85+
pattern: "spam",
86+
regex: "^(?:.+/)?spam(?:/.*)?$",
87+
files: [
88+
"spam",
89+
"spam/",
90+
"foo/spam",
91+
"spam/foo",
92+
"foo/spam/bar",
93+
],
94+
expectedResults: [
95+
"spam",
96+
"spam/",
97+
"foo/spam",
98+
"spam/foo",
99+
"foo/spam/bar",
100+
]
101+
)
102+
}
103+
104+
func testRelativeNested() throws {
105+
try _testRunner(
106+
pattern: "foo/spam",
107+
regex: "^foo/spam(?:/.*)?$",
108+
files: [
109+
"foo/spam",
110+
"foo/spam/bar",
111+
"bar/foo/spam",
112+
],
113+
expectedResults: [
114+
"foo/spam",
115+
"foo/spam/bar",
116+
]
117+
)
118+
}
119+
120+
func testChildDoubleAsterisk() throws {
121+
try _testRunner(
122+
pattern: "spam/**",
123+
regex: "^spam/.*$",
124+
files: [
125+
"spam/bar",
126+
"foo/spam/bar"
127+
],
128+
expectedResults: [
129+
"spam/bar"
130+
]
131+
)
132+
}
133+
134+
func testInnerDoubleAsterisk() throws {
135+
try _testRunner(
136+
pattern: "left/**/right",
137+
regex: "^left(?:/.+)?/right(?:/.*)?$",
138+
files: [
139+
"left/bar/right",
140+
"left/foo/bar/right",
141+
"left/bar/right/foo",
142+
"foo/left/bar/right",
143+
],
144+
expectedResults: [
145+
"left/bar/right",
146+
"left/foo/bar/right",
147+
"left/bar/right/foo",
148+
]
149+
)
150+
}
151+
152+
func testOnlyDoubleAsterisk() throws {
153+
try _testRunner(
154+
pattern: "**",
155+
regex: "^.+$",
156+
files: [],
157+
expectedResults: []
158+
)
159+
}
160+
161+
func testParentDoubleAsterisk() throws {
162+
try _testRunner(
163+
pattern: "**/spam",
164+
regex: "^(?:.+/)?spam(?:/.*)?$",
165+
files: [
166+
"foo/spam",
167+
"foo/spam/bar",
168+
],
169+
expectedResults: [
170+
"foo/spam",
171+
"foo/spam/bar",
172+
]
173+
)
174+
}
175+
176+
func testInfixWildcard() throws {
177+
try _testRunner(
178+
pattern: "foo-*-bar",
179+
regex: "^(?:.+/)?foo-[^/]*-bar(?:/.*)?$",
180+
files: [
181+
"foo--bar",
182+
"foo-hello-bar",
183+
"a/foo-hello-bar",
184+
"foo-hello-bar/b",
185+
"a/foo-hello-bar/b",
186+
],
187+
expectedResults: [
188+
"foo--bar",
189+
"foo-hello-bar",
190+
"a/foo-hello-bar",
191+
"foo-hello-bar/b",
192+
"a/foo-hello-bar/b",
193+
]
194+
)
195+
}
196+
197+
func testPostfixWildcard() throws {
198+
try _testRunner(
199+
pattern: "~temp-*",
200+
regex: "^(?:.+/)?~temp-[^/]*(?:/.*)?$",
201+
files: [
202+
"~temp-",
203+
"~temp-foo",
204+
"~temp-foo/bar",
205+
"foo/~temp-bar",
206+
"foo/~temp-bar/baz",
207+
],
208+
expectedResults: [
209+
"~temp-",
210+
"~temp-foo",
211+
"~temp-foo/bar",
212+
"foo/~temp-bar",
213+
"foo/~temp-bar/baz",
214+
]
215+
)
216+
}
217+
218+
func testPrefixWildcard() throws {
219+
try _testRunner(
220+
pattern: "*.swift",
221+
regex: "^(?:.+/)?[^/]*\\.swift(?:/.*)?$",
222+
files: [
223+
"bar.swift",
224+
"bar.swift/",
225+
"foo/bar.swift",
226+
"foo/bar.swift/baz",
227+
],
228+
expectedResults: [
229+
"bar.swift",
230+
"bar.swift/",
231+
"foo/bar.swift",
232+
"foo/bar.swift/baz",
233+
]
234+
)
235+
}
236+
237+
func testDirectory() throws {
238+
try _testRunner(
239+
pattern: "dir/",
240+
regex: "^(?:.+/)?dir/.*$",
241+
files: [
242+
"dir/",
243+
"foo/dir/",
244+
"foo/dir/bar",
245+
"dir",
246+
],
247+
expectedResults: [
248+
"dir/",
249+
"foo/dir/",
250+
"foo/dir/bar",
251+
]
252+
)
253+
}
254+
255+
func testFailingInitializers() throws {
256+
XCTAssertThrowsError(try GitIgnoreSpec(pattern: ""))
257+
XCTAssertThrowsError(try GitIgnoreSpec(pattern: "***"))
258+
}
259+
}

0 commit comments

Comments
 (0)