Skip to content

Yarn2: Improve dependency handling#11972

Open
oheger-bosch wants to merge 2 commits into
oss-review-toolkit:mainfrom
boschglobal:oheger-bosch/yarn2_dependencies
Open

Yarn2: Improve dependency handling#11972
oheger-bosch wants to merge 2 commits into
oss-review-toolkit:mainfrom
boschglobal:oheger-bosch/yarn2_dependencies

Conversation

@oheger-bosch

Copy link
Copy Markdown
Member

This PR addresses issues in the Yarn2+ implementation that were discovered in a project where dependencies with multiple versions were referenced. In this case, Yarn resolves a dependency version which might not match the one contained in locators, and thus the lookup for PackageInfo objects fails. With the changes provided here, such issues could be fixed.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.58%. Comparing base (9d51987) to head (81d9b7c).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...de/src/main/kotlin/yarn2/Yarn2DependencyHandler.kt 80.00% 1 Missing and 6 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #11972      +/-   ##
============================================
+ Coverage     58.44%   58.58%   +0.13%     
- Complexity     1811     1819       +8     
============================================
  Files           361      361              
  Lines         13504    13562      +58     
  Branches       1385     1404      +19     
============================================
+ Hits           7893     7945      +52     
- Misses         5115     5116       +1     
- Partials        496      501       +5     
Flag Coverage Δ
test-ubuntu-24.04 42.18% <80.00%> (+0.41%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@oheger-bosch oheger-bosch marked this pull request as ready for review June 10, 2026 07:22
@oheger-bosch oheger-bosch requested a review from a team as a code owner June 10, 2026 07:22

import java.io.File

import org.apache.logging.log4j.kotlin.logger

@sschuberth sschuberth Jun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When packages with different versions appear in the dependency graph, Yarn resolves a specific version.

Let me ensure I understand this sentence correctly. Are you saying, if a project transitively depends on package "foo" both in version "1.0.0" and "2.0.0", then only either version will be resolved?

If so, that would contradict my understand of the Node ecosystem's capability to indeed support multiple versions of the same package in the transitive tree.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have rephrased the commit message to hopefully make it clearer. The problem with the current implementation is that it looks up PackageInfo objects using a key which contains a specific version. In this project, however, there are references to dependencies with slightly different versions.

It has been observed that packages in the dependency graph reference
other packages with a different version than was actually installed.
The current implementation could not handle this correctly, but failed
to look up the `PackageInfo` for affected packages. Fix this by
implementing fallback logic for virtual packages and packages with
changed versions.

Signed-off-by: Oliver Heger <oliver.heger@bosch.com>
Handle the case that a package appears multiple times with different
versions in the dependency graph. In this case, try to select the
correct version based on semantic version ranges.

Signed-off-by: Oliver Heger <oliver.heger@bosch.com>
@oheger-bosch oheger-bosch force-pushed the oheger-bosch/yarn2_dependencies branch from 32e3553 to 81d9b7c Compare June 10, 2026 09:17

override fun dependenciesFor(dependency: PackageInfo): List<PackageInfo> =
dependency.children.dependencies.map { packageInfoForLocator.getValue(it.realLocator) }
dependency.children.dependencies.map(this::packageInfoFor)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please drop this.

}
})

private val WORKING_DIR = File(".")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be safer / better practice to use a temporary directory here?

Comment on lines +111 to +127
return when (candidates.size) {
1 -> candidates.single().also {
logger.debug {
"Resolved locator '${dependency.realLocator}' to '${it.value}' via module name lookup."
}
}

0 -> error(
"Could not find a PackageInfo for locator '${dependency.realLocator}'. No entry for module " +
"'$moduleName' exists in ${packageInfoForLocator.keys}."
)

else -> error(
"Could not unambiguously resolve locator '${dependency.realLocator}'. Found ${candidates.size} " +
"installed versions of module '$moduleName': ${candidates.map { it.value }}."
)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewriting this as

        candidates.singleOrNull()?.also {
            logger.debug {
                "Resolved locator '${dependency.realLocator}' to '${it.value}' via module name lookup."
            }

            return it
        }

        if (candidates.isEmpty()) {
            error(
                "Could not find a PackageInfo for locator '${dependency.realLocator}'. No entry for module " +
                    "'$moduleName' exists in ${packageInfoForLocator.keys}."
            )
        }

        error("Could not unambiguously resolve locator '${dependency.realLocator}'. Found ${candidates.size} " +
            "installed versions of module '$moduleName': ${candidates.map { it.value }}.")

probably is a bit more readable, and keep the diff to the next commit smaller.

Comment on lines +188 to +189
if (descriptorRemainder.startsWith("npm:")) {
val rangeSpec = descriptorRemainder.removePrefix("npm:")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using withoutPrefix().

Comment on lines +190 to +191
val range = runCatching { RangeListFactory.create(rangeSpec) }.getOrNull()
if (range != null) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer

        runCatching {
            RangeListFactory.create(rangeSpec)
        }.onSuccess { range ->

Comment on lines +196 to +197
if (matchingCandidates.size == 1) {
return matchingCandidates.single().also {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using singleOrNull() to not both check the size and eventually return the single value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants