diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index d34c539..181b062 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -11,12 +11,12 @@ on:
jobs:
test:
if: github.event.pull_request.draft == false
- runs-on: macos-14
+ runs-on: macos-26
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Select Xcode
- run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer
+ run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer
- name: Run tests
run: sudo swift test --parallel -Xswiftc -DCI -v
diff --git a/Package.resolved b/Package.resolved
index c36ddcf..5dd17a1 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,12 +1,13 @@
{
+ "originHash" : "176905f781cea79f89254540d531b56a9cc2c902c62fb2822303ec6d51ebd53e",
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
- "revision" : "41982a3656a71c768319979febd796c6fd111d5c",
- "version" : "1.5.0"
+ "revision" : "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b",
+ "version" : "1.7.1"
}
},
{
@@ -19,5 +20,5 @@
}
}
],
- "version" : 2
+ "version" : 3
}
diff --git a/Package.swift b/Package.swift
index 6b36696..100a5a0 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.2
import PackageDescription
diff --git a/README.md b/README.md
index abda727..22139d2 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ A tool that finds Xcode color assets by their hex codes. The idea behind this to
-
+
diff --git a/Sources/hexcode/Commands/Hexcode.swift b/Sources/hexcode/Commands/Hexcode.swift
index 3a4fbc0..1b40bfd 100644
--- a/Sources/hexcode/Commands/Hexcode.swift
+++ b/Sources/hexcode/Commands/Hexcode.swift
@@ -10,7 +10,7 @@ struct Hexcode: AsyncParsableCommand {
by their hexadecimal codes.
""",
usage: "hexcode [--directory ]",
- version: "hexcode 0.2.1",
+ version: "hexcode 0.2.2",
subcommands: [
FindColor.self,
FindDuplicates.self,
diff --git a/Sources/hexcode/Controllers/AssetCollector.swift b/Sources/hexcode/Controllers/AssetCollector.swift
index 3da5a24..98bca04 100644
--- a/Sources/hexcode/Controllers/AssetCollector.swift
+++ b/Sources/hexcode/Controllers/AssetCollector.swift
@@ -2,8 +2,6 @@ import Foundation
/// Finds color asset folders and converts them to Swift objects.
protocol AssetCollecting {
- /// `FileManager` instance used for the search.
- var fileManager: FileManager { get set }
/// Finds all color sets in the directory and its subdirectories.
/// - parameter directory: Path to the directory to search for color sets.
/// - throws: Errors when valid directory not found at given path or can't read content.
@@ -11,8 +9,14 @@ protocol AssetCollecting {
func collectAssets(in directory: String) async throws -> [NamedColorSet]
}
-final class AssetCollector: AssetCollecting {
- var fileManager: FileManager = .default
+final class AssetCollector: AssetCollecting, Sendable {
+ /// Judging from Apple's documentation, the use of the default shared `FileManager` from multiple threads is fine.
+ nonisolated(unsafe)
+ let fileManager: FileManager
+
+ init(fileManager: FileManager = .default) {
+ self.fileManager = fileManager
+ }
func collectAssets(in directory: String) async throws -> [NamedColorSet] {
var isDirectory: ObjCBool = false
@@ -55,7 +59,6 @@ extension AssetCollector {
private func findColorSets(
at paths: [String],
in searchRootDirectory: String,
- alreadyFoundColorSets: [NamedColorSet] = []
) async -> [NamedColorSet] {
let colorSets = await withTaskGroup(of: [NamedColorSet].self) { group in
for path in paths {
@@ -64,7 +67,7 @@ extension AssetCollector {
switch contentAtPath {
case .colorSet(let colorSet):
- return self.makeNamedColorset(
+ return Self.makeNamedColorset(
from: colorSet,
at: path,
in: searchRootDirectory
@@ -76,7 +79,6 @@ extension AssetCollector {
let colorSetsFromSubdirectory = await self.findColorSets(
at: fullSubpaths,
in: searchRootDirectory,
- alreadyFoundColorSets: alreadyFoundColorSets
)
return colorSetsFromSubdirectory
@@ -86,13 +88,13 @@ extension AssetCollector {
}
}
- return await group.reduce(alreadyFoundColorSets, +)
+ return await group.reduce([], +)
}
return colorSets
}
- private func makeNamedColorset(
+ private static func makeNamedColorset(
from colorSet: ColorSet,
at path: String,
in searchRootDirectory: String
@@ -119,7 +121,7 @@ extension AssetCollector {
try fileManager.contentsOfDirectory(atPath: directory)
}
- private func getAssetName(from path: String, in searchRootDirectory: String) -> String {
+ private static func getAssetName(from path: String, in searchRootDirectory: String) -> String {
let trimmedPath = String(path.trimmingPrefix(searchRootDirectory + "/"))
return URL(filePath: trimmedPath)
.deletingPathExtension()
diff --git a/Sources/hexcode/Controllers/ColorFinder.swift b/Sources/hexcode/Controllers/ColorFinder.swift
index b2c3c16..5845ace 100644
--- a/Sources/hexcode/Controllers/ColorFinder.swift
+++ b/Sources/hexcode/Controllers/ColorFinder.swift
@@ -45,7 +45,7 @@ final class ColorFinder: ColorFinding {
for currentColor in currentColors {
let rgbHex = currentColor.color.rgbHex
- if rgbHex.isEmpty || duplicates.keys.contains(rgbHex) {
+ if rgbHex.isEmpty || duplicates[rgbHex] != nil {
continue
}
@@ -81,9 +81,7 @@ final class ColorFinder: ColorFinding {
)
colorNames.append(currentColorSetName)
- if duplicates[rgbHex] == nil {
- duplicates[rgbHex] = colorNames.sorted()
- }
+ duplicates[rgbHex] = colorNames.sorted()
}
}
diff --git a/Sources/hexcode/HexcodeApp.swift b/Sources/hexcode/HexcodeApp.swift
index eaae35d..9d2e007 100644
--- a/Sources/hexcode/HexcodeApp.swift
+++ b/Sources/hexcode/HexcodeApp.swift
@@ -8,17 +8,14 @@ final class HexcodeApp {
init(
fileManager: FileManager = .default,
- assetCollector: AssetCollecting = AssetCollector(),
+ assetCollector: AssetCollecting? = nil,
colorFinder: ColorFinding = ColorFinder(),
outputFunction: @escaping ((String) -> Void) = { print($0) }
) {
self.fileManager = fileManager
self.colorFinder = colorFinder
self.output = outputFunction
-
- var assetCollector = assetCollector
- assetCollector.fileManager = fileManager
- self.assetCollector = assetCollector
+ self.assetCollector = assetCollector ?? AssetCollector(fileManager: fileManager)
}
/// Entry point for the default `find-color` subcommand logic.
diff --git a/Sources/hexcode/Models/ColorAsset+Color.swift b/Sources/hexcode/Models/ColorAsset+Color.swift
index e5831ef..0161fad 100644
--- a/Sources/hexcode/Models/ColorAsset+Color.swift
+++ b/Sources/hexcode/Models/ColorAsset+Color.swift
@@ -50,16 +50,11 @@ extension ColorAsset.Color {
}
private func isValidHexComponent(_ component: String) -> Bool {
- matchRegex(component, pattern: "0x[0-9a-fA-F]{2}")
- }
-
- private func matchRegex(_ string: String, pattern: String) -> Bool {
- guard let regex = try? NSRegularExpression(pattern: pattern) else { return false }
- let foundMatches = regex.matches(
- in: string,
+ let foundMatches = Self.hexRegex.matches(
+ in: component,
range: NSRange(
location: 0,
- length: string.count
+ length: component.count
)
)
return foundMatches.count == 1
@@ -87,6 +82,9 @@ extension ColorAsset.Color {
private static let floatConversionMultiplier: Float = 255.2
private static let hexFormatString = "%02X"
+ private static let hexRegex = try! NSRegularExpression(
+ pattern: "0x[0-9a-fA-F]{2}"
+ )
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.decimalSeparator = "."
diff --git a/Tests/hexcodeTests/AssetCollectorTests.swift b/Tests/hexcodeTests/AssetCollectorTests.swift
index 50f5b66..18f711e 100644
--- a/Tests/hexcodeTests/AssetCollectorTests.swift
+++ b/Tests/hexcodeTests/AssetCollectorTests.swift
@@ -12,12 +12,24 @@ final class AssetCollectorTests: XCTestCase {
override func setUp() {
mocks = Mocks()
- sut = SUT()
- sut.fileManager = mocks.fileManager
+ sut = SUT(fileManager: mocks.fileManager)
}
// MARK: - Tests
+ func test_init_withDefaultParameters_setsFileManagerToDefault() async throws {
+ // Given, When
+ sut = SUT()
+
+ // Then
+ XCTAssertTrue(sut.fileManager === FileManager.default)
+ }
+
+ func test_init_withNonDefaultFileManagerPassed_setsTheGivenFileManager() async throws {
+ // Then
+ XCTAssertTrue(sut.fileManager === mocks.fileManager)
+ }
+
func test_collectAssets_inAssetCatalogDirectory_findsAssets() async throws {
// Given
let catalogPath = "Assets.xcassets"
diff --git a/Tests/hexcodeTests/HexcodeAppTests.swift b/Tests/hexcodeTests/HexcodeAppTests.swift
index 6eb53f7..4e45c40 100644
--- a/Tests/hexcodeTests/HexcodeAppTests.swift
+++ b/Tests/hexcodeTests/HexcodeAppTests.swift
@@ -17,24 +17,6 @@ final class HexcodeAppTests: XCTestCase {
sut = makeSUT()
}
- // MARK: - Test init
-
- func test_init_withoutFileManager_passesDefaultFileManagerIntoAssetCollector() {
- // When
- _ = SUT(assetCollector: mocks.assetCollector)
-
- // Then
- XCTAssertEqual(mocks.assetCollector.calls, [.setFileManager(.default)])
- }
-
- func test_init_passingFileManager_passesDefaultFileManagerIntoAssetCollector() {
- // When
- _ = SUT(fileManager: mocks.fileManager, assetCollector: mocks.assetCollector)
-
- // Then
- XCTAssertEqual(mocks.assetCollector.calls, [.setFileManager(mocks.fileManager)])
- }
-
// MARK: - Test runFindColor
func test_runFindColor_withoutDirectory_runsInCurrentDirectoryFromFileManager() async throws {
diff --git a/Tests/hexcodeTests/Mocks/AssetCollectorMock.swift b/Tests/hexcodeTests/Mocks/AssetCollectorMock.swift
index 2dc2941..4bd2c65 100644
--- a/Tests/hexcodeTests/Mocks/AssetCollectorMock.swift
+++ b/Tests/hexcodeTests/Mocks/AssetCollectorMock.swift
@@ -4,13 +4,10 @@ import Foundation
final class AssetCollectorMock {
enum Call: Equatable {
case collectAssetsIn(directory: String)
- case getFileManager
- case setFileManager(FileManager)
}
struct CallResults {
var collectAssets: Result<[NamedColorSet], Error> = .success([])
- var fileManager: FileManager = FileManagerMock()
}
private(set) var calls: [Call] = []
@@ -25,17 +22,6 @@ final class AssetCollectorMock {
// MARK: - AssetCollecting
extension AssetCollectorMock: AssetCollecting {
- var fileManager: FileManager {
- get {
- calls.append(.getFileManager)
- return results.fileManager
- }
-
- set {
- calls.append(.setFileManager(newValue))
- }
- }
-
func collectAssets(in directory: String) throws -> [NamedColorSet] {
calls.append(.collectAssetsIn(directory: directory))
return try results.collectAssets.get()