Skip to content
Merged
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
7 changes: 6 additions & 1 deletion Sources/SPMParsing/PackageSwiftFileVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ final class PackageSwiftFileVisitor: SyntaxVisitor {
targetType = .regular
}

let pathArgument = targetCall.argumentList.first(where: { $0.label?.text == "path" })?
.expression.as(StringLiteralExprSyntax.self)?.segments.description
.trimmingCharacters(in: .punctuationCharacters)

let dependenciesArray = targetCall.argumentList.first(where: { $0.label?.text == "dependencies" })?.expression.as(ArrayExprSyntax.self)?.elements
let dependencies = dependenciesArray?.compactMap { element -> String? in
if let stringLiteral = element.expression.as(StringLiteralExprSyntax.self) {
Expand All @@ -76,7 +80,8 @@ final class PackageSwiftFileVisitor: SyntaxVisitor {
type: targetType,
dependencies: dependenciesSet,
duplicateDependencies: findDuplicateDependencies(dependencies),
layerNumber: layers[targetName]
layerNumber: layers[targetName],
path: pathArgument
)
targets.append(target)
}
Expand Down
28 changes: 25 additions & 3 deletions Sources/SPMParsing/PackagesParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,28 @@ final class PackagesParser {
guard target.duplicateDependencies.isEmpty else {
throw PackagesParser.Error.duplicateDependencies(targetName: target.name, dependencies: target.duplicateDependencies)
}
var swiftFilesPath = path + "/" + package.name + target.type.intermediatePath + target.name
if !FileManager.default.fileExists(atPath: swiftFilesPath) {
swiftFilesPath = path + "/" + package.name + target.type.intermediatePath
var swiftFilesPath: String
if let customPath = target.path {
// Normalize path: strip leading/trailing slashes to avoid double-slash issues
let normalizedPath = customPath
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
// Use explicit path from Package.swift
swiftFilesPath = path + "/" + package.name + "/" + normalizedPath
// Error if custom path doesn't exist - don't silently scan nothing
guard FileManager.default.fileExists(atPath: swiftFilesPath) else {
throw PackagesParser.Error.customPathNotFound(
targetName: target.name,
path: customPath,
resolvedPath: swiftFilesPath
)
}
} else {
// Fall back to convention: Sources/TargetName or Tests/TargetName
swiftFilesPath = path + "/" + package.name + target.type.intermediatePath + target.name
// Only fall back to parent directory if convention path doesn't exist
if !FileManager.default.fileExists(atPath: swiftFilesPath) {
swiftFilesPath = path + "/" + package.name + target.type.intermediatePath
}
}
let swiftFilesParser = SwiftFilesParser(
rootURL: URL(fileURLWithPath: swiftFilesPath),
Expand Down Expand Up @@ -95,13 +114,16 @@ extension PackagesParser {
enum Error: Swift.Error, CustomStringConvertible, Equatable {
case failedToParsePackage(path: String)
case duplicateDependencies(targetName: String, dependencies: [String])
case customPathNotFound(targetName: String, path: String, resolvedPath: String)

var description: String {
switch self {
case .failedToParsePackage(let path):
"Failed to parse Package.swift at path: \(path)"
case .duplicateDependencies(let targetName, let dependencies):
"❌ Target \(targetName) has duplicate dependencies: \(dependencies.joined(separator: ", "))"
case .customPathNotFound(let targetName, let path, let resolvedPath):
"❌ Target \(targetName) specifies path: \"\(path)\" but directory not found at: \(resolvedPath)"
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Sources/SPMParsing/SwiftPackageTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ struct SwiftPackageTarget: Equatable {
let dependencies: Set<String>
let duplicateDependencies: [String]
let layerNumber: Int?
let path: String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ struct DiagramBuilderTests {
type: .test,
dependencies: [],
duplicateDependencies: [],
layerNumber: nil
layerNumber: nil,
path: nil
)
]
)
Expand Down Expand Up @@ -173,7 +174,8 @@ struct DiagramBuilderTests {
type: .regular,
dependencies: [],
duplicateDependencies: [],
layerNumber: nil
layerNumber: nil,
path: nil
)
]
)
Expand Down Expand Up @@ -212,7 +214,8 @@ struct DiagramBuilderTests {
type: .regular,
dependencies: [],
duplicateDependencies: [],
layerNumber: 0
layerNumber: 0,
path: nil
)
]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Foundation
import XCTest
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Foundation
import CoreDependency
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Foundation
import UndeclaredDependency
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "CustomPathPackage",
dependencies: [],
targets: [
.target(
name: "TestModule",
dependencies: ["CoreDependency"],
path: "Sources/Core"
),
.testTarget(
name: "TestModuleTests",
dependencies: ["XCTest"],
path: "CustomTests"
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "InvalidPathPackage",
dependencies: [],
targets: [
.target(
name: "MissingModule",
dependencies: [],
path: "Sources/DoesNotExist"
)
]
)
59 changes: 59 additions & 0 deletions Tests/SwiftImportChecksTests/SPMParsing/PackagesParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,63 @@ struct PackagesParserTests {
)
#expect(messages == expectedMessages)
}

@Test("test parsePackages given valid path with custom path argument uses correct target path")
func parsePackagesGivenValidPathWithCustomPathArgument() throws {
// Given
// This test verifies that:
// 1. Files in Sources/Core are scanned for TestModule (has CoreDependency import)
// 2. Files in Sources/UI are NOT scanned (has UndeclaredDependency import that would fail)
// 3. Files in CustomTests are scanned for TestModuleTests (testTarget with custom path)
let path: String = URL.Mock.customPathPackageFileDir.relativePath
var messages: [String] = []
let expectedMessages: [String] = [
"Package: CustomPathPackage Target: TestModule - Type: regular",
"✅ All imports for target TestModule are explicit",
"Package: CustomPathPackage Target: TestModuleTests - Type: test",
"✅ All imports for target TestModuleTests are explicit"
]
let sut = makeSUT(path: path)

// When
try sut.parsePackages(
configs: configs,
verbose: verbose,
print: { messages.append($0) }
)

// Then
// If Sources/UI was incorrectly scanned, this would fail with UndeclaredDependency error
#expect(messages == expectedMessages)
}

@Test("test parsePackages given custom path that does not exist throws error")
func parsePackagesGivenCustomPathNotFoundThrowsError() throws {
// Given
let path: String = URL.Mock.invalidCustomPathPackageFileDir.relativePath
var messages: [String] = []
let expectedMessages: [String] = [
"Package: InvalidPathPackage Target: MissingModule - Type: regular"
]
let sut = makeSUT(path: path)

// When, Then
#expect(
throws: PackagesParser.Error.customPathNotFound(
targetName: "MissingModule",
path: "Sources/DoesNotExist",
resolvedPath: path + "/InvalidPathPackage/Sources/DoesNotExist"
),
performing: {
try sut.parsePackages(
configs: configs,
verbose: verbose,
print: { messages.append($0) }
)
}
)
#expect(messages == expectedMessages)
}
}

extension PackagesParserTests {
Expand All @@ -162,5 +219,7 @@ private extension URL {
static let secondPackageFileDir = Bundle.module.url(forResource: "Example/SecondPackage/Package", withExtension: "swift")!.deletingLastPathComponent()
static let duplicatesPackageFileDir = Bundle.module.url(forResource: "Example/DuplicatesPackage/Package", withExtension: "swift")!.deletingLastPathComponent()
static let failurePackageFileDir = Bundle.module.url(forResource: "Example/FailurePackage/Package", withExtension: "swift")!.deletingLastPathComponent()
static let customPathPackageFileDir = Bundle.module.url(forResource: "Example/CustomPathPackage/Package", withExtension: "swift")!.deletingLastPathComponent()
static let invalidCustomPathPackageFileDir = Bundle.module.url(forResource: "Example/InvalidCustomPathPackage/Package", withExtension: "swift")!.deletingLastPathComponent()
}
}