Skip to content

Commit

Permalink
Use alternate implementation of glob with globstar support
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 committed Mar 11, 2022
1 parent 2ae22d0 commit 3c2a46b
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 32 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
107 changes: 101 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,96 @@ struct Glob {
return [pattern]
}

defer { isDirectoryCache.removeAll() }

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 var isDirectoryCache = [String: Bool]()

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 {
if let isDirectory = isDirectoryCache[path] {
return isDirectory
}

var isDirectoryBool = ObjCBool(false)
var isDirectory = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectoryBool)
isDirectory = isDirectory && isDirectoryBool.boolValue

isDirectoryCache[path] = isDirectory

return isDirectory
}

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) }
}
}
}
4 changes: 2 additions & 2 deletions Tests/SwiftLintFrameworkTests/ConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -443,12 +443,12 @@ extension ConfigurationTests {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(
includedPaths: ["Level1"],
excludedPaths: ["Level1/**/*.swift", "Level1/**/**/*.swift"])
excludedPaths: ["Level1/**/*.swift"])
let paths = configuration.lintablePaths(inPath: "Level1",
forceExclude: false,
excludeByPrefix: true)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(filenames, ["Level1.swift"])
XCTAssertEqual(filenames, [])
}

func testDictInitWithCachePath() throws {
Expand Down
27 changes: 15 additions & 12 deletions Tests/SwiftLintFrameworkTests/GlobTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Foundation
@testable import SwiftLintFramework
import XCTest

Expand Down Expand Up @@ -53,8 +54,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 +63,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 3c2a46b

Please sign in to comment.