Skip to content

Commit

Permalink
Use alternate implementation of glob with globstar support (#3892)
Browse files Browse the repository at this point in the history
Adapted from https://gist.github.com/efirestone/ce01ae109e08772647eb061b3bb387c3

The implementation from Pathos seems buggy.

Fixes #3891
  • Loading branch information
jpsim authored Mar 11, 2022
1 parent 2ae22d0 commit 1abc503
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 31 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@

* Support recursive globs.
[funzin](https://github.com/funzin)
[JP Simard](https://github.com/jpsim)
[#3789](https://github.com/realm/SwiftLint/issues/3789)
[#3891](https://github.com/realm/SwiftLint/issues/3891)

#### Bug Fixes

Expand Down
9 changes: 0 additions & 9 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
{
"object": {
"pins": [
{
"package": "Pathos",
"repositoryURL": "https://github.com/dduan/Pathos",
"state": {
"branch": null,
"revision": "8697a340a25e9974d4bbdee80a4c361c74963c00",
"version": "0.4.2"
}
},
{
"package": "SourceKitten",
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
Expand Down
4 changes: 1 addition & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ let package = Package(
.package(url: "https://github.com/jpsim/SourceKitten.git", from: "0.31.1"),
.package(url: "https://github.com/jpsim/Yams.git", from: "4.0.2"),
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
.package(url: "https://github.com/dduan/Pathos", from: "0.4.2")
] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.4.3"))] : []),
targets: [
.executableTarget(
Expand All @@ -42,9 +41,8 @@ let package = Package(
name: "SwiftLintFramework",
dependencies: [
.product(name: "SourceKittenFramework", package: "SourceKitten"),
"Pathos",
"SwiftSyntax",
"Yams"
"Yams",
]
+ (addCryptoSwift ? ["CryptoSwift"] : [])
+ (staticSwiftSyntax ? ["lib_InternalSwiftSyntaxParser"] : [])
Expand Down
95 changes: 89 additions & 6 deletions Source/SwiftLintFramework/Helpers/Glob.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import Foundation
import Pathos

#if canImport(Darwin)
import Darwin

private let globFunction = Darwin.glob
#elseif canImport(Glibc)
import Glibc

private let globFunction = Glibc.glob
#else
#error("Unsupported platform")
#endif

// Adapted from https://gist.github.com/efirestone/ce01ae109e08772647eb061b3bb387c3

struct Glob {
static func resolveGlob(_ pattern: String) -> [String] {
Expand All @@ -8,14 +21,84 @@ struct Glob {
return [pattern]
}

return expandGlobstar(pattern: pattern)
.reduce(into: [String]()) { paths, pattern in
var globResult = glob_t()
defer { globfree(&globResult) }

if globFunction(pattern, GLOB_TILDE | GLOB_BRACE | GLOB_MARK, nil, &globResult) == 0 {
paths.append(contentsOf: populateFiles(globResult: globResult))
}
}
.unique
.sorted()
.map { $0.absolutePathStandardized() }
}

// MARK: Private

private static func expandGlobstar(pattern: String) -> [String] {
guard pattern.contains("**") else {
return [pattern]
}

var results = [String]()
var parts = pattern.components(separatedBy: "**")
let firstPart = parts.removeFirst()
var lastPart = parts.joined(separator: "**")

let fileManager = FileManager.default

var directories: [String]

let searchPath = firstPart.isEmpty ? fileManager.currentDirectoryPath : firstPart
do {
let paths = try Path(pattern).glob()
return try paths.compactMap { path in
try path.absolute().description
directories = try fileManager.subpathsOfDirectory(atPath: searchPath).compactMap { subpath in
let fullPath = firstPart.bridge().appendingPathComponent(subpath)
guard isDirectory(path: fullPath) else { return nil }
return fullPath
}
} catch {
queuedPrintError(error.localizedDescription)
return []
directories = []
queuedPrintError("Error parsing file system item: \(error)")
}

// Check the base directory for the glob star as well.
directories.insert(firstPart, at: 0)

// Include the globstar root directory ("dir/") in a pattern like "dir/**" or "dir/**/"
if lastPart.isEmpty {
results.append(firstPart)
lastPart = "*"
}

for directory in directories {
let partiallyResolvedPattern: String
if directory.isEmpty {
partiallyResolvedPattern = lastPart.starts(with: "/") ? String(lastPart.dropFirst()) : lastPart
} else {
partiallyResolvedPattern = directory.bridge().appendingPathComponent(lastPart)
}
results.append(contentsOf: expandGlobstar(pattern: partiallyResolvedPattern))
}

return results
}

private static func isDirectory(path: String) -> Bool {
var isDirectoryBool = ObjCBool(false)
let isDirectory = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectoryBool)
return isDirectory && isDirectoryBool.boolValue
}

private static func populateFiles(globResult: glob_t) -> [String] {
#if os(Linux)
let matchCount = globResult.gl_pathc
#else
let matchCount = globResult.gl_matchc
#endif
return (0..<Int(matchCount)).compactMap { index in
globResult.gl_pathv[index].flatMap { String(validatingUTF8: $0) }
}
}
}
2 changes: 1 addition & 1 deletion Tests/SwiftLintFrameworkTests/ConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ extension ConfigurationTests {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(
includedPaths: ["Level1"],
excludedPaths: ["Level1/**/*.swift", "Level1/**/**/*.swift"])
excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"])
let paths = configuration.lintablePaths(inPath: "Level1",
forceExclude: false,
excludeByPrefix: true)
Expand Down
26 changes: 14 additions & 12 deletions Tests/SwiftLintFrameworkTests/GlobTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ final class GlobTests: XCTestCase {
]

let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("*.swift"))
XCTAssertEqual(files.count, 2)
XCTAssertEqual(Set(files), expectedFiles)
XCTAssertEqual(files.sorted(), expectedFiles.sorted())
}

func testMatchesNestedDirectory() {
Expand All @@ -63,17 +62,20 @@ final class GlobTests: XCTestCase {
}

func testGlobstarSupport() {
let expectedFiles: Set = [
mockPath.stringByAppendingPathComponent("Directory.swift/DirectoryLevel1.swift"),
mockPath.stringByAppendingPathComponent("Level1/Level1.swift"),
mockPath.stringByAppendingPathComponent("Level1/Level2/Level2.swift"),
mockPath.stringByAppendingPathComponent("Level1/Level2/Level3/Level3.swift"),
mockPath.stringByAppendingPathComponent("NestedConfig/Test/Main.swift"),
mockPath.stringByAppendingPathComponent("NestedConfig/Test/Sub/Sub.swift")
]
let expectedFiles = Set(
[
"Directory.swift",
"Directory.swift/DirectoryLevel1.swift",
"Level0.swift",
"Level1/Level1.swift",
"Level1/Level2/Level2.swift",
"Level1/Level2/Level3/Level3.swift",
"NestedConfig/Test/Main.swift",
"NestedConfig/Test/Sub/Sub.swift"
].map(mockPath.stringByAppendingPathComponent)
)

let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("**/*.swift"))
XCTAssertEqual(files.count, 6)
XCTAssertEqual(Set(files), expectedFiles)
XCTAssertEqual(files.sorted(), expectedFiles.sorted())
}
}

0 comments on commit 1abc503

Please sign in to comment.