Skip to content

Commit 3bb87a0

Browse files
authored
Correct "Did you mean XYZ" messages when a dependency is not found. (#8303)
1 parent 4e029ce commit 3bb87a0

File tree

2 files changed

+309
-42
lines changed

2 files changed

+309
-42
lines changed

Sources/PackageGraph/ModulesGraph+Loading.swift

Lines changed: 109 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -685,31 +685,11 @@ private func createResolvedPackages(
685685
// found errors when there are more important errors to
686686
// resolve (like authentication issues).
687687
if !observabilityScope.errorsReportedInAnyScope {
688-
// Emit error if a product (not module) declared in the package is also a productRef (dependency)
689-
let declProductsAsDependency = package.products.filter { product in
690-
lookupByProductIDs ? product.identity == productRef.identity : product.name == productRef.name
691-
}.map {$0.modules}.flatMap{$0}.filter { t in
692-
t.name != productRef.name
693-
}
694-
695-
// Find a product name from the available product dependencies that is most similar to the required product name.
696-
let bestMatchedProductName = bestMatch(for: productRef.name, from: Array(allModuleNames))
697-
var packageContainingBestMatchedProduct: String?
698-
if let bestMatchedProductName, productRef.name == bestMatchedProductName {
699-
let dependentPackages = packageBuilder.dependencies.map(\.package)
700-
for p in dependentPackages where p.modules.contains(where: { $0.name == bestMatchedProductName }) {
701-
packageContainingBestMatchedProduct = p.identity.description
702-
break
703-
}
704-
}
705-
let error = PackageGraphError.productDependencyNotFound(
706-
package: package.identity.description,
707-
moduleName: moduleBuilder.module.name,
708-
dependencyProductName: productRef.name,
709-
dependencyPackageName: productRef.package,
710-
dependencyProductInDecl: !declProductsAsDependency.isEmpty,
711-
similarProductName: bestMatchedProductName,
712-
packageContainingSimilarProduct: packageContainingBestMatchedProduct
688+
let error = prepareProductDependencyNotFoundError(
689+
packageBuilder: packageBuilder,
690+
moduleBuilder: moduleBuilder,
691+
dependency: productRef,
692+
lookupByProductIDs: lookupByProductIDs
713693
)
714694
packageObservabilityScope.emit(error)
715695
}
@@ -838,6 +818,110 @@ private func createResolvedPackages(
838818
return IdentifiableSet(try packageBuilders.map { try $0.construct() })
839819
}
840820

821+
private func prepareProductDependencyNotFoundError(
822+
packageBuilder: ResolvedPackageBuilder,
823+
moduleBuilder: ResolvedModuleBuilder,
824+
dependency: Module.ProductReference,
825+
lookupByProductIDs: Bool
826+
) -> PackageGraphError {
827+
let packageName = packageBuilder.package.identity.description
828+
// Module's dependency is either a local module or a product from another package.
829+
// If dependency is a product from the current package, that's an incorrect
830+
// declaration of the dependency and we should show relevant error. Let's see
831+
// if indeed the dependency matches any of the products.
832+
let declProductsAsDependency = packageBuilder.package.products.filter { product in
833+
lookupByProductIDs ? product.identity == dependency.identity : product.name == dependency.name
834+
}.flatMap(\.modules).filter { t in
835+
t.name != dependency.name
836+
}
837+
if !declProductsAsDependency.isEmpty {
838+
return PackageGraphError.productDependencyNotFound(
839+
package: packageName,
840+
moduleName: moduleBuilder.module.name,
841+
dependencyProductName: dependency.name,
842+
dependencyPackageName: dependency.package,
843+
dependencyProductInDecl: true,
844+
similarProductName: nil,
845+
packageContainingSimilarProduct: nil
846+
)
847+
}
848+
849+
// If dependency name is a typo, find best possible match from the available destinations.
850+
// Depending on how the dependency is declared, "available destinations" might be:
851+
// - modules within the current package
852+
// - products across all packages in the graph
853+
// - products from a specific package
854+
var packageContainingBestMatchedProduct: String?
855+
var bestMatchedProductName: String?
856+
if dependency.package == nil {
857+
// First assume it's a dependency on modules within the same package.
858+
let localModules = Array(packageBuilder.modules.map(\.module.name).filter { $0 != moduleBuilder.module.name })
859+
bestMatchedProductName = bestMatch(for: dependency.name, from: localModules)
860+
if bestMatchedProductName != nil {
861+
return PackageGraphError.productDependencyNotFound(
862+
package: packageName,
863+
moduleName: moduleBuilder.module.name,
864+
dependencyProductName: dependency.name,
865+
dependencyPackageName: nil,
866+
dependencyProductInDecl: false,
867+
similarProductName: bestMatchedProductName,
868+
packageContainingSimilarProduct: nil
869+
)
870+
}
871+
// Since there's no package name in the dependency declaration, and no match across
872+
// the local modules, we assume the user actually meant to use product dependency,
873+
// but didn't specify package to use the product from. Since products are globally
874+
// unique, we should be able to find a good match across the graph, if the package
875+
// is already a part of the dependency tree.
876+
let availableProducts = Dictionary(
877+
uniqueKeysWithValues: packageBuilder.dependencies
878+
.flatMap { (packageDep: ResolvedPackageBuilder) -> [(
879+
String,
880+
String
881+
)] in
882+
let manifestProducts = packageDep.package.manifest.products.map(\.name)
883+
let explicitProducts = packageDep.package.products.filter { manifestProducts.contains($0.name) }
884+
let explicitIdsOrNames = Set(explicitProducts.map { lookupByProductIDs ? $0.identity : $0.name })
885+
return explicitIdsOrNames.map { ($0, packageDep.package.identity.description) }
886+
}
887+
)
888+
bestMatchedProductName = bestMatch(for: dependency.name, from: Array(availableProducts.keys))
889+
if bestMatchedProductName != nil {
890+
packageContainingBestMatchedProduct = availableProducts[bestMatchedProductName!]
891+
}
892+
return PackageGraphError.productDependencyNotFound(
893+
package: packageName,
894+
moduleName: moduleBuilder.module.name,
895+
dependencyProductName: dependency.name,
896+
dependencyPackageName: nil,
897+
dependencyProductInDecl: false,
898+
similarProductName: bestMatchedProductName,
899+
packageContainingSimilarProduct: packageContainingBestMatchedProduct
900+
)
901+
} else {
902+
// Package is explicitly listed in the product dependency, we shall search
903+
// within the products from that package.
904+
let availableProducts = packageBuilder.dependencies
905+
.filter { $0.package.identity.description == dependency.package }
906+
.flatMap { (packageDep: ResolvedPackageBuilder) -> [String] in
907+
let manifestProducts = packageDep.package.manifest.products.map(\.name)
908+
let explicitProducts = packageDep.package.products.filter { manifestProducts.contains($0.name) }
909+
let explicitIdsOrNames = Set(explicitProducts.map { lookupByProductIDs ? $0.identity : $0.name })
910+
return Array(explicitIdsOrNames)
911+
}
912+
bestMatchedProductName = bestMatch(for: dependency.name, from: availableProducts)
913+
return PackageGraphError.productDependencyNotFound(
914+
package: packageName,
915+
moduleName: moduleBuilder.module.name,
916+
dependencyProductName: dependency.name,
917+
dependencyPackageName: dependency.package,
918+
dependencyProductInDecl: false,
919+
similarProductName: bestMatchedProductName,
920+
packageContainingSimilarProduct: dependency.package
921+
)
922+
}
923+
}
924+
841925
private func emitDuplicateProductDiagnostic(
842926
productName: String,
843927
packages: [Package],

0 commit comments

Comments
 (0)