diff --git a/Sources/PackageGraph/ModulesGraph+Loading.swift b/Sources/PackageGraph/ModulesGraph+Loading.swift index a056875684a..5c0ab4e4ea1 100644 --- a/Sources/PackageGraph/ModulesGraph+Loading.swift +++ b/Sources/PackageGraph/ModulesGraph+Loading.swift @@ -434,6 +434,13 @@ private func createResolvedPackages( // Track if multiple targets are found with the same name. var foundDuplicateTarget = false + for packageBuilder in packageBuilders { + for target in packageBuilder.targets { + // Record if we see a duplicate target. + foundDuplicateTarget = foundDuplicateTarget || !allTargetNames.insert(target.target.name).inserted + } + } + // Do another pass and establish product dependencies of each target. for packageBuilder in packageBuilders { let package = packageBuilder.package @@ -493,9 +500,6 @@ private func createResolvedPackages( // Establish dependencies in each target. for targetBuilder in packageBuilder.targets { - // Record if we see a duplicate target. - foundDuplicateTarget = foundDuplicateTarget || !allTargetNames.insert(targetBuilder.target.name).inserted - // Directly add all the system module dependencies. targetBuilder.dependencies += implicitSystemTargetDeps.map { .target($0, conditions: []) } @@ -524,13 +528,22 @@ private func createResolvedPackages( } else { // Find a product name from the available product dependencies that is most similar to the required product name. let bestMatchedProductName = bestMatch(for: productRef.name, from: Array(allTargetNames)) + var packageContainingBestMatchedProduct: String? + if let bestMatchedProductName, productRef.name == bestMatchedProductName { + let dependentPackages = packageBuilder.dependencies.map(\.package) + for p in dependentPackages where p.targets.contains(where: { $0.name == bestMatchedProductName }) { + packageContainingBestMatchedProduct = p.identity.description + break + } + } let error = PackageGraphError.productDependencyNotFound( package: package.identity.description, targetName: targetBuilder.target.name, dependencyProductName: productRef.name, dependencyPackageName: productRef.package, dependencyProductInDecl: !declProductsAsDependency.isEmpty, - similarProductName: bestMatchedProductName + similarProductName: bestMatchedProductName, + packageContainingSimilarProduct: packageContainingBestMatchedProduct ) packageObservabilityScope.emit(error) } @@ -568,7 +581,7 @@ private func createResolvedPackages( // If a target with similar name was encountered before, we emit a diagnostic. if foundDuplicateTarget { var duplicateTargets = [String: [Package]]() - for targetName in allTargetNames.sorted() { + for targetName in Set(allTargetNames).sorted() { let packages = packageBuilders .filter({ $0.targets.contains(where: { $0.target.name == targetName }) }) .map{ $0.package } diff --git a/Sources/PackageGraph/ModulesGraph.swift b/Sources/PackageGraph/ModulesGraph.swift index 471d6192e5e..84f1bcd4907 100644 --- a/Sources/PackageGraph/ModulesGraph.swift +++ b/Sources/PackageGraph/ModulesGraph.swift @@ -23,7 +23,7 @@ enum PackageGraphError: Swift.Error { case cycleDetected((path: [Manifest], cycle: [Manifest])) /// The product dependency not found. - case productDependencyNotFound(package: String, targetName: String, dependencyProductName: String, dependencyPackageName: String?, dependencyProductInDecl: Bool, similarProductName: String?) + case productDependencyNotFound(package: String, targetName: String, dependencyProductName: String, dependencyPackageName: String?, dependencyProductInDecl: Bool, similarProductName: String?, packageContainingSimilarProduct: String?) /// The package dependency already satisfied by a different dependency package case dependencyAlreadySatisfiedByIdentifier(package: String, dependencyLocation: String, otherDependencyURL: String, identity: PackageIdentity) @@ -230,12 +230,14 @@ extension PackageGraphError: CustomStringConvertible { (cycle.path + cycle.cycle).map({ $0.displayName }).joined(separator: " -> ") + " -> " + cycle.cycle[0].displayName - case .productDependencyNotFound(let package, let targetName, let dependencyProductName, let dependencyPackageName, let dependencyProductInDecl, let similarProductName): + case .productDependencyNotFound(let package, let targetName, let dependencyProductName, let dependencyPackageName, let dependencyProductInDecl, let similarProductName, let packageContainingSimilarProduct): if dependencyProductInDecl { return "product '\(dependencyProductName)' is declared in the same package '\(package)' and can't be used as a dependency for target '\(targetName)'." } else { var description = "product '\(dependencyProductName)' required by package '\(package)' target '\(targetName)' \(dependencyPackageName.map{ "not found in package '\($0)'" } ?? "not found")." - if let similarProductName { + if let similarProductName, let packageContainingSimilarProduct { + description += " Did you mean '.product(name: \"\(similarProductName)\", package: \"\(packageContainingSimilarProduct)\")'?" + } else if let similarProductName { description += " Did you mean '\(similarProductName)'?" } return description diff --git a/Tests/PackageGraphTests/ModulesGraphTests.swift b/Tests/PackageGraphTests/ModulesGraphTests.swift index efae57d71a4..420387d5d7d 100644 --- a/Tests/PackageGraphTests/ModulesGraphTests.swift +++ b/Tests/PackageGraphTests/ModulesGraphTests.swift @@ -2676,6 +2676,57 @@ final class ModulesGraphTests: XCTestCase { XCTAssertEqual(observability.diagnostics.count, 0, "unexpected diagnostics: \(observability.diagnostics.map { $0.description })") } + + func testDependencyResolutionWithErrorMessages() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/aaa/Sources/aaa/main.swift", + "/zzz/Sources/zzz/source.swift" + ) + + let observability = ObservabilitySystem.makeForTesting() + let _ = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "aaa", + path: "/aaa", + dependencies: [ + .localSourceControl(path: "/zzz", requirement: .upToNextMajor(from: "1.0.0")) + ], + products: [], + targets: [ + TargetDescription( + name: "aaa", + dependencies: ["zzy"], + type: .executable + ) + ]), + Manifest.createRootManifest( + displayName: "zzz", + path: "/zzz", + products: [ + ProductDescription( + name: "zzz", + type: .library(.automatic), + targets: ["zzz"] + ) + ], + targets: [ + TargetDescription( + name: "zzz" + ) + ]) + ], + observabilityScope: observability.topScope + ) + + testDiagnostics(observability.diagnostics) { result in + result.check( + diagnostic: "product 'zzy' required by package 'aaa' target 'aaa' not found. Did you mean 'zzz'?", + severity: .error + ) + } + } }