Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions Sources/ProjectDrivers/SPMProjectDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public final class SPMProjectDriver {
private let pkg: SPM.Package
private let configuration: Configuration
private let logger: Logger
private let interfaceBuilderFileExtensions: Set<String> = ["storyboard", "xib"]

public convenience init(configuration: Configuration, shell: Shell, logger: Logger) throws {
if !configuration.schemes.isEmpty {
Expand Down Expand Up @@ -80,24 +81,49 @@ extension SPMProjectDriver: ProjectDriver {

for target in description.targets {
let targetPath = pkg.path.appending(target.path)
xibFiles.formUnion(interfaceBuilderFiles(in: targetPath))

guard let resources = target.resources else { continue }

for resource in resources {
// Resource.path is always a single file path
let resourceFilePath = FilePath(resource.path)
let resourcePath: FilePath = resourceFilePath.isAbsolute
? resourceFilePath
: targetPath.appending(resource.path)

// Check if the resource path exists and is a xib/storyboard file
guard resourcePath.exists else { continue }
guard let ext = resourcePath.extension?.lowercased(), ["xib", "storyboard"].contains(ext) else { continue }
guard let ext = resourcePath.extension?.lowercased(), interfaceBuilderFileExtensions.contains(ext) else { continue }

xibFiles.insert(resourcePath)
}
}

return xibFiles
}

private func interfaceBuilderFiles(in targetPath: FilePath) -> Set<FilePath> {
guard targetPath.exists else { return [] }
guard let enumerator = FileManager.default.enumerator(
at: targetPath.url,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]
) else {
return []
}

var xibFiles: Set<FilePath> = []

Comment thread
ileitch marked this conversation as resolved.
for case let url as URL in enumerator {
guard let isRegularFile = try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile,
isRegularFile == true
else { continue }

let path = FilePath(url.path).lexicallyNormalized()
guard let ext = path.extension?.lowercased(), interfaceBuilderFileExtensions.contains(ext) else { continue }

xibFiles.insert(path)
}

return xibFiles
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
</buttonCell>
<connections>
<action selector="buttonTapped:" target="-2" id="jLb-bl-k6b"/>
<action selector="privateExtensionTapped:" target="-2" id="private-extension-action"/>
</connections>
</button>
</subviews>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public final class SPMXibViewController: NSViewController {

@IBOutlet var button: NSButton!

@IBAction func buttonTapped(_: Any) {
@IBAction private func buttonTapped(_: Any) {
showAlert(title: "SPMXibViewController", message: "buttonTapped(_:) action triggered!")
}

Expand All @@ -15,7 +15,7 @@ public final class SPMXibViewController: NSViewController {

@IBOutlet var unusedMacOutlet: NSTextField!

@IBAction func unusedMacAction(_: Any) {
@IBAction private func unusedMacAction(_: Any) {
showAlert(title: "SPMXibViewController", message: "unusedMacAction(_:) - this should be reported as unused!")
}

Expand All @@ -36,3 +36,13 @@ public final class SPMXibViewController: NSViewController {
alert.runModal()
}
}

private extension SPMXibViewController {
@IBAction func privateExtensionTapped(_: Any) {
showAlert(title: "SPMXibViewController", message: "privateExtensionTapped(_:) action triggered!")
}

@IBAction func unusedPrivateExtensionAction(_: Any) {
showAlert(title: "SPMXibViewController", message: "unusedPrivateExtensionAction(_:) - this should be reported as unused!")
}
}
62 changes: 62 additions & 0 deletions Tests/SPMTests/SPMProjectMacOSTest.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#if os(macOS)
import Configuration
import Foundation
import ProjectDrivers
import SystemPackage
@testable import TestShared
import XCTest

Expand All @@ -15,13 +18,72 @@
assertReferenced(.class("SPMXibViewController")) {
// Referenced via XIB (connected)
self.assertReferenced(.functionMethodInstance("buttonTapped(_:)"))
self.assertReferenced(.functionMethodInstance("privateExtensionTapped(_:)"))
self.assertReferenced(.varInstance("button"))
self.assertReferenced(.varInstance("borderWidth"))
// Unreferenced - not connected in XIB
self.assertNotReferenced(.varInstance("unusedMacOutlet"))
self.assertNotReferenced(.functionMethodInstance("unusedMacAction(_:)"))
self.assertNotReferenced(.functionMethodInstance("unusedPrivateExtensionAction(_:)"))
self.assertNotReferenced(.varInstance("unusedMacInspectable"))
}
}
}

final class SPMProjectMacOSFilesystemIBDiscoveryTest: SPMSourceGraphTestCase {
override static func setUp() {
super.setUp()
build(projectPath: SPMProjectMacOSPath)
}

func testDiscoversInterfaceBuilderFilesWithoutDeclaredResources() throws {
let manifestJSON = """
{
"targets": [
{
"name": "SPMProjectMacOSKit",
"type": "regular",
"path": "Sources/SPMProjectMacOSKit"
},
{
"name": "Frontend",
"type": "executable",
"path": "Sources/Frontend"
}
]
}
"""

let manifestURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("json")
try manifestJSON.write(to: manifestURL, atomically: true, encoding: .utf8)
defer {
try? FileManager.default.removeItem(at: manifestURL)
}

let planConfiguration = Configuration()
planConfiguration.jsonPackageManifestPath = FilePath(manifestURL.path)

try SPMProjectMacOSPath.chdir {
let driver = try SPMProjectDriver(
configuration: planConfiguration,
shell: Self.shell,
logger: Self.logger
)
Self.plan = try driver.plan(logger: Self.logger.contextualized(with: "index"))
}

let expectedXibPath = SPMProjectMacOSPath
.appending("Sources/SPMProjectMacOSKit/Resources/SPMXibViewController.xib")
XCTAssertTrue(Self.plan.xibPaths.contains(expectedXibPath))

index(configuration: Configuration())

assertReferenced(.class("SPMXibViewController")) {
self.assertReferenced(.functionMethodInstance("buttonTapped(_:)"))
self.assertReferenced(.functionMethodInstance("privateExtensionTapped(_:)"))
}
}
}
#endif
Loading