Skip to content

Support multi-document pnpm lockfiles - #1871

Open
RKS (rksharma-owg) wants to merge 16 commits into
microsoft:mainfrom
rksharma-owg:fix/pnpm-multi-document-lockfile
Open

RKS (rksharma-owg) wants to merge 16 commits into
microsoft:mainfrom
rksharma-owg:fix/pnpm-multi-document-lockfile

Conversation

@rksharma-owg

@rksharma-owg RKS (rksharma-owg) commented Sep 17, 2026 •

Copy link
Copy Markdown

Description

Valid pnpm lockfiles can contain an environment document followed by a project document. The single-document parser rejects the second document and skips the lockfile. Both inventories are now parsed and their components and dependency references recorded.

The shared parser accepts one or two nonempty documents in source order and returns an ordered read-only collection. Version detection uses that parser and rejects inconsistent declared versions; the unused internal single-document method is removed. Tests verify explicit environment dependencies and duplicate-package production classification and graph edges across v5/v6/v9; the new v5/v9 controls cover both document orders. Legacy versionless shrinkwrap handling is retained. Detector version is incremented to 9.

Fixes #1864.

Validation

Validated publication commit da6bc69d33a7dc0990fd53cdc1ec91bbf323441e, containing correction 7102ad7a8c8f3525fc3c5941b34a54d28e199365 merged with current main 2f00ab9b7def197310deec187f87ad0c52825f57.

  • Test-first proof: 11 new behavioral cases failed on the previous implementation; all 50 focused pnpm tests pass after the correction.
  • Full tests including Integration and Cobertura: 1,512 passed and eight existing platform skips on Linux and Windows. Build/analyzers report zero warnings/errors; recorded coverage does not decrease against current main.
  • Snapshot verification: all six tests passed with no skips on Linux, Windows, and macOS.
  • C# CodeQL completed; downloaded SARIF contains zero findings. The upstream smoke command succeeded across its twelve repositories. Empty fixture lockfiles are rejected as intended; existing malformed-fixture diagnostics remain visible.

Older-base snapshot failures involve four unrelated DotNet/NuGet components and reproduce identically on the untouched previous PR head. The published current-main merge passes all snapshot checks. Full local Apple Silicon tests retain one Vcpkg path-normalization failure, also reproduced on untouched current main; required hosted Linux/Windows tests pass. The fork runs contain those older-base/control failures, so the successful final-commit jobs are linked individually. Upstream review, Codecov status, and protected orchestration remain pending.

Checklist

  • Regression tests added or updated
  • Build and analyzers passed
  • Applicable fork validation completed for the published commit
  • Related issue linked

- Use YamlDotNet Parser to deserialize all documents from multi-document pnpm lockfiles
- Validate lockfile version consistency across documents, failing clearly on conflicting versions
- Record components and dependencies across all documents in Pnpm9, Pnpm6, and Pnpm5 detectors
- Support packageManagerDependencies and configDependencies from environment lockfile documents
- Bump PnpmComponentDetectorFactory version to 9
Copilot AI lite review requested due to automatic review settings September 17, 2026 03:53
@rksharma-owg
RKS (rksharma-owg) requested a review from a team as a code owner September 17, 2026 03:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Add dependency-edge assertions and focused v5 multi-document test coverage.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds multi-document pnpm lockfile support, version validation, cross-document dependency handling, and related tests.

Changes:

  • Parses and validates all YAML documents.
  • Aggregates components and dependency metadata across documents.
  • Adds environment dependency fields for v6 and v9.
  • Updates v5/v6/v9 detectors and bumps the detector version.
File summaries
File Summary
test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs Tests multi-document parsing and version validation.
test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs Tests multi-document detector scenarios.
src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs Bumps the detector version.
src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs Processes v9 documents and environment dependencies.
src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs Processes v6 documents and dependencies.
src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs Processes v5 documents.
src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmParsingUtilitiesFactory.cs Parses versions across YAML documents.
src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmParsingUtilitiesBase.cs Adds multi-document deserialization.
src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmHasDependenciesV9.cs Adds v9 environment dependency properties.
src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmHasDependenciesV6.cs Adds v6 environment dependency properties.
Review details

Suppressed comments (2)

src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs:19

  • This changes the v5 detector to consume every YAML document, but there is no multi-document v5 detector test. Add a focused fixture that verifies components and dependency edges from both documents; otherwise a regression in this new loop would not be detected.
        foreach (var yaml in yamls)
        {
            foreach (var packageKeyValue in yaml?.Packages ?? Enumerable.Empty<KeyValuePair<string, Package>>())

src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs:75

  • The v6 multi-document test only checks component count and names. Since packages are registered in the earlier pass, it would pass even if this new importer dependency-processing loop were removed; add assertions for explicit references from both documents to cover the behavior introduced here.
        foreach (var yaml in yamls)
        {
            // "dedicated shrinkwrap" (single package) case:
            this.ProcessDependencySet(singleFileComponentRecorder, components, yaml);

            // "shared shrinkwrap" (workspace / mono-repos) case:
            foreach (var (_, package) in yaml.Importers ?? Enumerable.Empty<KeyValuePair<string, PnpmHasDependenciesV6>>())
            {
                this.ProcessDependencySet(singleFileComponentRecorder, components, package);
            }
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +84 to +85
this.ProcessDependencyList(singleFileComponentRecorder, components, item.PackageManagerDependencies);
this.ProcessDependencyList(singleFileComponentRecorder, components, item.ConfigDependencies);
Comment on lines +64 to +68
foreach (var yaml in yamls)
{
this.ProcessDependencySets(singleFileComponentRecorder, components, package);
foreach (var (_, package) in yaml.Importers ?? Enumerable.Empty<KeyValuePair<string, PnpmHasDependenciesV9>>())
{
this.ProcessDependencySets(singleFileComponentRecorder, components, package);
Copilot AI review requested due to automatic review settings September 17, 2026 11:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Fix duplicate v6 package-path handling across documents before approval.

Review details

Suppressed comments (1)

src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs:46

  • When the same v6 package path occurs in both YAML documents, this guard keeps only the first document's Package and its dev metadata. The later document is therefore not registered, and ProcessDependencyList also looks up the first tuple, so an environment/project overlap that is production in one document and development in the other can be classified incorrectly (and later package metadata/dependency edges are lost). Preserve/merge package metadata per document rather than dropping repeated paths; in particular, the effective dev value should let a production occurrence win.
                if (!components.ContainsKey(pnpmDependencyPath))
                {
                    var parentDetectedComponent = this.pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath(pnpmPackagePath: pnpmDependencyPath);
                    components.Add(pnpmDependencyPath, (parentDetectedComponent, package));

                    // Register the component.
                    // It should get registered again with with additional information (what depended on it) later,
                    // but registering it now ensures nothing is missed due to a limitation in dependency traversal
                    // like skipping local dependencies which might have transitively depended on this.
                    singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: this.pnpmParsingUtilities.IsPnpmPackageDevDependency(package));
                }
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…pendencies assertions

- In Pnpm6Detector, merge package metadata across documents so production occurrences take precedence over development occurrences, and merge declared child dependencies across documents.
- In Pnpm9Detector, merge snapshot dependencies across documents when the same snapshot path occurs across multiple documents.
- In PnpmDetectorTests, exercise non-empty configDependencies for v6 and v9 multi-document scenarios and assert explicit reference status.
- Add TestPnpmDetector_V6_MultiDocumentLockfile_DuplicatePackage_ProductionWinsAsync to verify dev vs prod resolution and merged dependency edges.
Copilot AI review requested due to automatic review settings September 17, 2026 11:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Critical unresolved dependency references may be silently dropped in the pnpm v6 and v9 detectors.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs:130

  • A missing dependency path is now ignored, so a non-local dependency that cannot be resolved from any document is omitted while the detector still succeeds. The file:/link: cases are filtered immediately above, and the old indexed lookup intentionally surfaced other misses; please retain that failure or report the unresolved reference instead of silently producing an incomplete graph.
            if (!components.TryGetValue(pnpmDependencyPath, out var componentAndPackage))
            {
                continue;

src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs:161

  • This change makes unresolved non-local transitive dependencies disappear silently. The file:, link:, and URL cases are filtered before this lookup, so a miss here means the graph cannot resolve a declared dependency; returning success without the edge causes an incomplete SBOM. Please keep the previous failing lookup (or explicitly surface unresolved references) rather than continuing.
            if (!components.TryGetValue(pnpmDependencyPath, out var componentAndPackage))
            {
                continue;
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs Outdated
Comment thread src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs Outdated
…d v9

Retain direct indexed dictionary lookup components[pnpmDependencyPath] when walking non-local package and indirect dependencies in Pnpm6Detector and Pnpm9Detector so that unresolved references in malformed lockfiles continue to fail fast rather than silently producing an incomplete dependency graph.
Copilot AI review requested due to automatic review settings September 17, 2026 12:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

All reviewed changes are covered by tests with no unresolved blocking issues.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

* Promote MSBuildBinaryLog Detector to prod
* Make Swift Detector experimental

* Expand Package.resolved data reporting

* Serialization constructor

* Fix test constructors

* Fix serialization issue

* PR feedback

* Emit SwiftComponent afterall

* Fix test expectations

* Make the SwiftComponent Id truely representative

* Don't include extranious ID information
* Fix Go direct dependency classification

Mark go.mod requirements as explicitly referenced unless they carry the // indirect marker.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b9f67746-b363-43ac-9c8c-764b1890c63c

* Bump Go detector version

Increment the detector version because explicit-reference metadata now changes for go.mod dependencies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b9f67746-b363-43ac-9c8c-764b1890c63c

---------

Co-authored-by: Aayush Maini <aamaini@microsoft.com>
Copilot-Session: b9f67746-b363-43ac-9c8c-764b1890c63c
* Fix Cargo.lock Git source dependency matching

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Skip fork-only detector reminder failure and bump Rust detector version

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Don't report Pip for conda component

* Cleanup

* Fix outdated comment
* Resolve Maven properties in component coordinates

Resolve Maven property references in group IDs and artifact IDs as well as versions when using static POM parsing. Skip components whose coordinates remain unresolved and bump the detector version for the output change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Resolve nested Maven property references

Recursively resolve coordinate properties with memoization and cycle detection. Add coverage for nested local and inherited properties and cyclic definitions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bound Maven property expansion depth

Limit recursive coordinate property expansion to prevent deeply nested POM properties from exhausting the process stack. Leave over-depth coordinates unresolved so they are skipped safely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Reject malformed Maven property references

Treat any residual Maven interpolation marker as unresolved so malformed references are skipped rather than emitted as component coordinates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Aayush Maini <aamaini@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add exception handling for non-Dockerfile formats and add tests for non-dockerfiles

* Don't check for dockerfiles in `node_modules` or subfolders of it.

* Add logging verification for Shiki language definition test
* Swift relies on Version, not hash

* PR feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some questions and comments. I don't have a pnpm background so some of this may just be me misunderstanding the intricacies, feel free to correct me with some docs if so.

parser.Consume<StreamStart>();

var versions = new List<string>();
while (parser.TryConsume<DocumentStart>(out _))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know it wasn't this way before, but given that things have gotten a bit more complicated now I think it's better if this calls out to DeserializePnpmYamlFileDocuments (unless I'm missing some difference).

parser.TryConsume<DocumentEnd>(out _);
}

return documents;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This accepts an arbitrary number of Documents (0-∞), while the pnpm lockfile spec you linked specifies that the valid values are 1 or 2.

return documents;
}

public T DeserializePnpmYamlFile(string fileContent)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems this is not used, I think we should remove it to avoid silently dropping the second document.

where T : PnpmYaml
{
public T DeserializePnpmYamlFile(string fileContent)
public virtual List<T> DeserializePnpmYamlFileDocuments(string fileContent)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
public virtual List<T> DeserializePnpmYamlFileDocuments(string fileContent)
public virtual IOrderedEnumerable<T> DeserializePnpmYamlFileDocuments(string fileContent)

We want to communicate that (as per the doc) order matters here, and we also want to make sure that nobody is modifying this list after it's been creating (unlikely, but if we're changing the type for ordering we may as well make that change too).

}

[TestMethod]
public async Task TestPnpmDetector_V6_MultiDocumentLockfile_DuplicatePackage_ProductionWinsAsync()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Curious why we have this code and test for V6 but not 5 or 9? Is this some PNPM intricacy that I'm missing?

If it's not an actual difference in how things behave between the different lockfile versions I think that it points toward a need to centralize the versions of the various detectors onto something like PnpmDetectorBase which does all the "common" stuff (dependecy de-duping, looping through documents, etc) and calls out to virtual methods for the stuff that's specific to each lockfile/pnpm version. That's unfortunately a bigger lift, so maybe just filing an issue for that and then making sure the different versions have consistent test coverage unless you're feeling up to it.

Copilot AI lite review requested due to automatic review settings October 6, 2026 17:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved Swift API compatibility, detector category compatibility, and public detector type compatibility issues remain.

Review effort: Lite
Findings: 2 High severity · 2 Low severity

Open (4)

public class SwiftComponent : TypedComponent
{
private readonly Uri packageUrl;
[JsonPropertyName("name")]
services.AddSingleton<IComponentDetector, NuGetComponentDetector>();
services.AddSingleton<IComponentDetector, NuGetPackagesConfigDetector>();
services.AddSingleton<IComponentDetector, NuGetProjectModelProjectCentricComponentDetector>();
services.AddSingleton<IComponentDetector, MSBuildBinaryLogComponentDetector>();
jobs:
comment:
# Fork pushes cannot find PRs opened against the upstream repository.
if: github.event.repository.fork == false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think something went wrong with your merge for these things to be showing up as diffs.

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.

[Bug]: pnpm detector rejects multi-document lockfiles

7 participants