Skip to content

Commit

Permalink
Merge pull request #2 from regexident/improve-init
Browse files Browse the repository at this point in the history
Improvements
  • Loading branch information
g-Off committed Apr 27, 2020
2 parents a6a74a2 + 7dd4ca5 commit 094c87f
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 268 deletions.
44 changes: 33 additions & 11 deletions Sources/Pathspec/GitIgnoreSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,25 @@
import Foundation

struct GitIgnoreSpec: Spec {
enum Error: Swift.Error {
case emptyPattern
case commented
case invalid
case emptyRoot
}

private(set) var inclusive: Bool = true

let pattern: String
let regex: NSRegularExpression

init?(pattern: String) {
guard !pattern.isEmpty else { return nil }
guard !pattern.hasPrefix("#") else { return nil }
guard !pattern.contains("***") else { return nil }
guard pattern != "/" else { return nil }

init(pattern: String) throws {
self.pattern = pattern

guard !pattern.isEmpty else { throw Error.emptyPattern }
guard !pattern.hasPrefix("#") else { throw Error.commented }
guard !pattern.contains("***") else { throw Error.invalid }
guard pattern != "/" else { throw Error.emptyRoot }

var pattern = pattern
if pattern.hasPrefix("!") {
Expand Down Expand Up @@ -81,11 +92,7 @@ struct GitIgnoreSpec: Spec {

regexString += "$"

do {
regex = try NSRegularExpression(pattern: regexString, options: [])
} catch {
return nil
}
regex = try NSRegularExpression(pattern: regexString, options: [])
}

func match(file: String) -> Bool {
Expand Down Expand Up @@ -152,3 +159,18 @@ struct GitIgnoreSpec: Spec {
return regex
}
}

extension GitIgnoreSpec: CustomStringConvertible {
var description: String {
let pattern = self.pattern.debugDescription
return "<\(type(of: self)) pattern: \(pattern)>"
}
}

extension GitIgnoreSpec: CustomDebugStringConvertible {
var debugDescription: String {
let pattern = self.pattern.debugDescription
let regexPattern = self.regex.pattern.debugDescription
return "<\(type(of: self)) pattern: \(pattern) regex: \(regexPattern)>"
}
}
40 changes: 36 additions & 4 deletions Sources/Pathspec/Pathspec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
public final class Pathspec {
private let specs: [Spec]

public init(patterns: String...) {
specs = patterns.compactMap {
GitIgnoreSpec(pattern: $0)
}
public convenience init(patterns: [String]) throws {
self.init(specs: try patterns.map {
try GitIgnoreSpec(pattern: $0)
})
}

public init(specs: [Spec]) {
self.specs = specs
}

public func match(path: String) -> Bool {
let matchingSpecs = self.matchingSpecs(path: path)
Expand All @@ -24,3 +28,31 @@ public final class Pathspec {
return specs.filter { $0.match(file: path) }
}
}

extension Pathspec: ExpressibleByArrayLiteral {
public typealias ArrayLiteralElement = String

public convenience init(arrayLiteral: String...) {
self.init(specs: arrayLiteral.compactMap {
try? GitIgnoreSpec(pattern: $0)
})
}
}

extension Pathspec: CustomStringConvertible {
public var description: String {
let specsDescription = specs.map { spec in
" " + String(describing: spec)
}.joined(separator: ",\n")
return "<\(type(of: self)) specs: [\n\(specsDescription)\n]>"
}
}

extension Pathspec: CustomDebugStringConvertible {
public var debugDescription: String {
let specsDescription = specs.map { spec in
" " + String(reflecting: spec)
}.joined(separator: ",\n")
return "<\(type(of: self)) specs: [\n\(specsDescription)\n]>"
}
}
259 changes: 259 additions & 0 deletions Tests/PathspecTests/GitIgnoreSpecTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
//
// PathspecTests.swift
// Pathspec
//
// Created by Geoffrey Foster on 2019-06-29.
//

import XCTest
@testable import Pathspec

final class GitIgnoreSpecTests: XCTestCase {
func testDescription() throws {
let spec = try XCTUnwrap(GitIgnoreSpec(pattern: "foobar"))

XCTAssertEqual(spec.description, "<GitIgnoreSpec pattern: \"foobar\">")
}

func testDebugDescription() throws {
let spec = try XCTUnwrap(GitIgnoreSpec(pattern: "foobar"))

XCTAssertEqual(spec.debugDescription, "<GitIgnoreSpec pattern: \"foobar\" regex: \"^(?:.+/)?foobar(?:/.*)?$\">")
}

func testAbsoluteRoot() throws {
XCTAssertThrowsError(try GitIgnoreSpec(pattern: "/"))
}

func testComment() throws {
XCTAssertThrowsError(try GitIgnoreSpec(pattern: "# Cork soakers."))
}

func testIgnore() throws {
let spec = try XCTUnwrap(GitIgnoreSpec(pattern: "!temp"))
XCTAssertFalse(spec.inclusive)
XCTAssertEqual(spec.regex.pattern, "^(?:.+/)?temp$")
let result = spec.match(file: "temp/foo")
XCTAssertEqual(result, false)
}

// MARK: - Inclusive tests

@inline(__always)
private func _testRunner(pattern: String, regex: String, files: [String], expectedResults: [String], file: StaticString = #file, line: UInt = #line) throws {
let spec = try XCTUnwrap(GitIgnoreSpec(pattern: pattern), file: file, line: line)
XCTAssertTrue(spec.inclusive, file: file, line: line)
XCTAssertEqual(spec.regex.pattern, regex, file: file, line: line)
let results = spec.match(files: files)
XCTAssertEqual(results, expectedResults, file: file, line: line)
}

func testAbsolute() throws {
try _testRunner(
pattern: "/an/absolute/file/path",
regex: "^an/absolute/file/path(?:/.*)?$",
files: [
"an/absolute/file/path",
"an/absolute/file/path/foo",
"foo/an/absolute/file/path",
],
expectedResults: [
"an/absolute/file/path",
"an/absolute/file/path/foo",
]
)
}

func testAbsoluteSingleItem() throws {
try _testRunner(
pattern: "/an/",
regex: "^an/.*$",
files: [
"an/absolute/file/path",
"an/absolute/file/path/foo",
"foo/an/absolute/file/path",
],
expectedResults: [
"an/absolute/file/path",
"an/absolute/file/path/foo",
]
)
}

func testRelative() throws {
try _testRunner(
pattern: "spam",
regex: "^(?:.+/)?spam(?:/.*)?$",
files: [
"spam",
"spam/",
"foo/spam",
"spam/foo",
"foo/spam/bar",
],
expectedResults: [
"spam",
"spam/",
"foo/spam",
"spam/foo",
"foo/spam/bar",
]
)
}

func testRelativeNested() throws {
try _testRunner(
pattern: "foo/spam",
regex: "^foo/spam(?:/.*)?$",
files: [
"foo/spam",
"foo/spam/bar",
"bar/foo/spam",
],
expectedResults: [
"foo/spam",
"foo/spam/bar",
]
)
}

func testChildDoubleAsterisk() throws {
try _testRunner(
pattern: "spam/**",
regex: "^spam/.*$",
files: [
"spam/bar",
"foo/spam/bar"
],
expectedResults: [
"spam/bar"
]
)
}

func testInnerDoubleAsterisk() throws {
try _testRunner(
pattern: "left/**/right",
regex: "^left(?:/.+)?/right(?:/.*)?$",
files: [
"left/bar/right",
"left/foo/bar/right",
"left/bar/right/foo",
"foo/left/bar/right",
],
expectedResults: [
"left/bar/right",
"left/foo/bar/right",
"left/bar/right/foo",
]
)
}

func testOnlyDoubleAsterisk() throws {
try _testRunner(
pattern: "**",
regex: "^.+$",
files: [],
expectedResults: []
)
}

func testParentDoubleAsterisk() throws {
try _testRunner(
pattern: "**/spam",
regex: "^(?:.+/)?spam(?:/.*)?$",
files: [
"foo/spam",
"foo/spam/bar",
],
expectedResults: [
"foo/spam",
"foo/spam/bar",
]
)
}

func testInfixWildcard() throws {
try _testRunner(
pattern: "foo-*-bar",
regex: "^(?:.+/)?foo-[^/]*-bar(?:/.*)?$",
files: [
"foo--bar",
"foo-hello-bar",
"a/foo-hello-bar",
"foo-hello-bar/b",
"a/foo-hello-bar/b",
],
expectedResults: [
"foo--bar",
"foo-hello-bar",
"a/foo-hello-bar",
"foo-hello-bar/b",
"a/foo-hello-bar/b",
]
)
}

func testPostfixWildcard() throws {
try _testRunner(
pattern: "~temp-*",
regex: "^(?:.+/)?~temp-[^/]*(?:/.*)?$",
files: [
"~temp-",
"~temp-foo",
"~temp-foo/bar",
"foo/~temp-bar",
"foo/~temp-bar/baz",
],
expectedResults: [
"~temp-",
"~temp-foo",
"~temp-foo/bar",
"foo/~temp-bar",
"foo/~temp-bar/baz",
]
)
}

func testPrefixWildcard() throws {
try _testRunner(
pattern: "*.swift",
regex: "^(?:.+/)?[^/]*\\.swift(?:/.*)?$",
files: [
"bar.swift",
"bar.swift/",
"foo/bar.swift",
"foo/bar.swift/baz",
],
expectedResults: [
"bar.swift",
"bar.swift/",
"foo/bar.swift",
"foo/bar.swift/baz",
]
)
}

func testDirectory() throws {
try _testRunner(
pattern: "dir/",
regex: "^(?:.+/)?dir/.*$",
files: [
"dir/",
"foo/dir/",
"foo/dir/bar",
"dir",
],
expectedResults: [
"dir/",
"foo/dir/",
"foo/dir/bar",
]
)
}

func testFailingInitializers() throws {
XCTAssertThrowsError(try GitIgnoreSpec(pattern: ""))
XCTAssertThrowsError(try GitIgnoreSpec(pattern: "***"))
}
}
Loading

0 comments on commit 094c87f

Please sign in to comment.