@@ -33,6 +33,9 @@ import org.ossreviewtoolkit.plugins.packagemanagers.node.NodePackageManagerType
3333import org.ossreviewtoolkit.plugins.packagemanagers.node.PackageJson
3434import org.ossreviewtoolkit.plugins.packagemanagers.node.parsePackage
3535
36+ import org.semver4j.Semver
37+ import org.semver4j.range.RangeListFactory
38+
3639internal class Yarn2DependencyHandler (
3740 private val moduleInfoResolver : ModuleInfoResolver
3841) : DependencyHandler<PackageInfo> {
@@ -86,13 +89,15 @@ internal class Yarn2DependencyHandler(
8689 * locator from the virtual package's actual resolved version (handles virtual packages whose `children.version`
8790 * was overridden by Yarn's `resolutions` feature).
8891 *
89- * If both targeted lookups fail, fall back to searching the map for all installed versions of the same module
90- * by name. This handles the case where Yarn's `resolutions` feature (or similar mechanisms) cause a non-virtual
91- * dependency locator to reference a version that is not present in the map, while a different version of the
92- * same module was actually installed. If exactly one candidate is found, it is used. If multiple candidates are
93- * found, the resolution is ambiguous and an exception is thrown.
92+ * If both targeted lookups fail, fall back to searching the map for all installed non-virtual, non-project
93+ * versions of the same module by name. This handles the case where Yarn's `resolutions` feature (or similar
94+ * mechanisms) cause a non-virtual dependency locator to reference a version that is not present in the map,
95+ * while a different version of the same module was actually installed. If exactly one candidate is found, it
96+ * is used. If multiple candidates are found, the semver range from the [dependency]'s descriptor is used to
97+ * narrow down the candidates. If after all fallbacks the result is still not unique, an exception is thrown.
9498 */
9599 internal fun packageInfoFor (dependency : PackageInfo .Dependency ): PackageInfo {
100+ // Direct lookup by the real locator.
96101 packageInfoForLocator[dependency.realLocator]?.let { return it }
97102
98103 // Fallback for virtual packages: derive the real locator from the virtual package's resolved version.
@@ -101,30 +106,34 @@ internal class Yarn2DependencyHandler(
101106 packageInfoForLocator[" $moduleName @npm:${virtualInfo.children.version} " ]?.let { return it }
102107 }
103108
104- // Fallback for version mismatches caused by Yarn's `resolutions` feature: find the single installed version
105- // of the same module by name, ignoring the exact version in the locator.
109+ // Fallback for version mismatches caused by Yarn's `resolutions` feature: find installed versions of the
110+ // same module by name, ignoring the exact version in the locator.
106111 val moduleName = Locator .parse(dependency.realLocator).moduleName
107112 val candidates = packageInfoForLocator.values.filter {
108113 it.moduleName == moduleName && ! it.isProject && ! it.isVirtual
109114 }
110115
111- return when (candidates.size) {
112- 1 -> candidates.single().also {
116+ if (candidates.size == 1 ) {
117+ return candidates.single().also {
113118 logger.debug {
114119 " Resolved locator '${dependency.realLocator} ' to '${it.value} ' via module name lookup."
115120 }
116121 }
122+ }
117123
118- 0 -> error(
119- " Could not find a PackageInfo for locator '${dependency.realLocator} '. No entry for module " +
120- " '$moduleName ' exists in ${packageInfoForLocator.keys} ."
121- )
124+ if (candidates.size > 1 ) {
125+ candidates.matchVersionRange(dependency)?.let { return it }
122126
123- else -> error(
127+ error(
124128 " Could not unambiguously resolve locator '${dependency.realLocator} '. Found ${candidates.size} " +
125129 " installed versions of module '$moduleName ': ${candidates.map { it.value }} ."
126130 )
127131 }
132+
133+ error(
134+ " Could not find a PackageInfo for locator '${dependency.realLocator} '. No entry for module " +
135+ " '$moduleName ' exists in ${packageInfoForLocator.keys} ."
136+ )
128137 }
129138}
130139
@@ -169,3 +178,31 @@ internal data class Locator(
169178
170179 val isVirtual: Boolean = remainder.startsWith(" virtual:" ) && ! isProject
171180}
181+
182+ /* *
183+ * Try to find a single [PackageInfo] from this collection that matches the given [dependency] taking semantic
184+ * version ranges into account.
185+ */
186+ private fun Collection<PackageInfo>.matchVersionRange (dependency : PackageInfo .Dependency ): PackageInfo ? {
187+ val descriptorRemainder = Locator .parse(dependency.descriptor).remainder
188+ if (descriptorRemainder.startsWith(" npm:" )) {
189+ val rangeSpec = descriptorRemainder.removePrefix(" npm:" )
190+ val range = runCatching { RangeListFactory .create(rangeSpec) }.getOrNull()
191+ if (range != null ) {
192+ val matchingCandidates = filter { candidate ->
193+ Semver .coerce(candidate.children.version)?.let { range.isSatisfiedBy(it) } == true
194+ }
195+
196+ if (matchingCandidates.size == 1 ) {
197+ return matchingCandidates.single().also {
198+ logger.debug {
199+ " Resolved locator '${dependency.realLocator} ' to '${it.value} ' via semver range " +
200+ " matching on descriptor '${dependency.descriptor} '."
201+ }
202+ }
203+ }
204+ }
205+ }
206+
207+ return null
208+ }
0 commit comments