From ea1730b6dca9e3460a068a177f062d23f5e2b464 Mon Sep 17 00:00:00 2001 From: k-kohey Date: Sat, 30 Mar 2024 22:57:02 +0900 Subject: [PATCH] Improve error message about the PackageGraphError.productDependencyNotFound (#7419) Fixed a bug in the productDependencyNotFound error message ### Motivation: fix https://github.com/apple/swift-package-manager/issues/7398 The above issue suggests the following two defects. 1. package dependency resolution changes depending on the order of packages (alphabetical order) 2. the phrase "Did you meen..." in the error message in the error message is not on target. ### Modifications: The first problem is as described in the issue, if users rename the directory containing Package.swift from repro to zzz, the "Did you meen..." will appear. will appear. Essentially, the error minutes should be displayed in full, regardless of the name of the directory. This is due to the fact that the loop used to resolve dependencies depends on the alphabetical order of the directories and the side effect on allTargetName inside the loop. Therefore, the side effect for allTargetName inside the loop has been moved to the outside of the loop. The second problem is that when dependency A of a package is not found, a strange suggestion "Did you meen `A`? ", which is a strange suggestion. This is because when a dependency is not found, the message "Did you meen ``" is printed if there is a dependency with a similar name (even the exact same name). Thus, if the names are the same, "Did you meen `.product(name: ... , package: "swift-argugument")"`" instead of the found target name. ### Result: Command to execute: `swift build` Result of command:. `error: 'repro': product 'ArgumentParser' required by package 'repro' target 'repro' not found. Did you mean '.product(name: "ArgumentParser", package: "swift-argument-parser")'?` Condition: The following steps were taken to create the environment. 1. mkdir repro 1. cd repro 1. swift package init --type executable 1. Open Package.swift and make sure it has this content: ```swift // swift-tools-version: 5.10 import PackageDescription let package = Package( name: "repro", dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0") ], targets: [ .executableTarget( name: "repro", dependencies: ["ArgumentParser"] ) ] ) ``` --------- Co-authored-by: Yuta Saito --- .../PackageGraph/ModulesGraph+Loading.swift | 23 +++++++-- Sources/PackageGraph/ModulesGraph.swift | 8 +-- .../PackageGraphTests/ModulesGraphTests.swift | 51 +++++++++++++++++++ 3 files changed, 74 insertions(+), 8 deletions(-) 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 + ) + } + } }