diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/InvalidParseVersionTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/InvalidParseVersionTelemetryRecord.cs
new file mode 100644
index 000000000..341b0b69b
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/InvalidParseVersionTelemetryRecord.cs
@@ -0,0 +1,14 @@
+namespace Microsoft.ComponentDetection.Common.Telemetry.Records;
+
+public class InvalidParseVersionTelemetryRecord : BaseDetectionTelemetryRecord
+{
+ public override string RecordName => "InvalidParseVersion";
+
+ public string DetectorId { get; set; }
+
+ public string FilePath { get; set; }
+
+ public string Version { get; set; }
+
+ public string MaxVersion { get; set; }
+}
diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PipReportVersionTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PipReportVersionTelemetryRecord.cs
deleted file mode 100644
index 26242d459..000000000
--- a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PipReportVersionTelemetryRecord.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Microsoft.ComponentDetection.Common.Telemetry.Records;
-
-public class PipReportVersionTelemetryRecord : BaseDetectionTelemetryRecord
-{
- public override string RecordName => "PipReportVersion";
-
- public string Version { get; set; }
-
- public string MaxVersion { get; set; }
-}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs
index 2bb07e1b9..5e074786e 100644
--- a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs
@@ -96,8 +96,10 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID
report.Version,
MaxReportVersion);
- using var versionRecord = new PipReportVersionTelemetryRecord
+ using var versionRecord = new InvalidParseVersionTelemetryRecord
{
+ DetectorId = this.Id,
+ FilePath = file.Location,
Version = report.Version,
MaxVersion = MaxReportVersion.ToString(),
};
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs
new file mode 100644
index 000000000..4c2695dc3
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs
@@ -0,0 +1,16 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using Microsoft.ComponentDetection.Contracts;
+
+///
+/// Interface that represents a version of the pnpm detector.
+///
+public interface IPnpmDetector
+{
+ ///
+ /// Parses a yaml file content in pnmp format into the dependecy graph.
+ ///
+ /// Content of the yaml file that contains the pnpm dependencies.
+ /// Component recorder to which to write the dependency graph.
+ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder);
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs
new file mode 100644
index 000000000..b91403ffe
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs
@@ -0,0 +1,58 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.ComponentDetection.Contracts;
+
+public class Pnpm5Detector : IPnpmDetector
+{
+ public const string MajorVersion = "5";
+
+ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder)
+ {
+ var yaml = PnpmParsingUtilities.DeserializePnpmYamlV5File(yamlFileContent);
+
+ foreach (var packageKeyValue in yaml.Packages ?? Enumerable.Empty>())
+ {
+ // Ignore file: as these are local packages.
+ if (packageKeyValue.Key.StartsWith("file:"))
+ {
+ continue;
+ }
+
+ var parentDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5(pnpmPackagePath: packageKeyValue.Key);
+ var isDevDependency = packageKeyValue.Value != null && PnpmParsingUtilities.IsPnpmPackageDevDependency(packageKeyValue.Value);
+ singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: isDevDependency);
+ parentDetectedComponent = singleFileComponentRecorder.GetComponent(parentDetectedComponent.Component.Id);
+
+ if (packageKeyValue.Value.Dependencies != null)
+ {
+ foreach (var dependency in packageKeyValue.Value.Dependencies)
+ {
+ // Ignore local packages.
+ if (PnpmParsingUtilities.IsLocalDependency(dependency))
+ {
+ continue;
+ }
+
+ var childDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5(
+ pnpmPackagePath: PnpmParsingUtilities.CreatePnpmPackagePathFromDependencyV5(dependency.Key, dependency.Value));
+
+ // Older code used the root's dev dependency value. We're leaving this null until we do a second pass to look at each components' top level referrers.
+ singleFileComponentRecorder.RegisterUsage(childDetectedComponent, parentComponentId: parentDetectedComponent.Component.Id, isDevelopmentDependency: null);
+ }
+ }
+ }
+
+ // PNPM doesn't know at the time of RegisterUsage being called for a dependency whether something is a dev dependency or not, so after building up the graph we look at top level referrers.
+ foreach (var component in singleFileComponentRecorder.GetDetectedComponents())
+ {
+ var graph = singleFileComponentRecorder.DependencyGraph;
+ var explicitReferences = graph.GetExplicitReferencedDependencyIds(component.Key);
+ foreach (var explicitReference in explicitReferences)
+ {
+ singleFileComponentRecorder.RegisterUsage(component.Value, isDevelopmentDependency: graph.IsDevelopmentDependency(explicitReference));
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6ComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs
similarity index 63%
rename from src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6ComponentDetector.cs
rename to src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs
index 088f42201..0a3e5a99e 100644
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6ComponentDetector.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs
@@ -1,82 +1,17 @@
namespace Microsoft.ComponentDetection.Detectors.Pnpm;
-using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
-using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
-using Microsoft.ComponentDetection.Contracts.Internal;
-using Microsoft.ComponentDetection.Contracts.TypedComponent;
-using Microsoft.Extensions.Logging;
-public class Pnpm6ComponentDetector : FileComponentDetector, IExperimentalDetector
+public class Pnpm6Detector : IPnpmDetector
{
- public Pnpm6ComponentDetector(
- IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
- IObservableDirectoryWalkerFactory walkerFactory,
- ILogger logger)
- {
- this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
- this.Scanner = walkerFactory;
- this.Logger = logger;
- }
-
- public override string Id { get; } = "Pnpm6";
-
- public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) };
-
- public override IList SearchPatterns { get; } = new List { "shrinkwrap.yaml", "pnpm-lock.yaml" };
-
- public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm };
+ public const string MajorVersion = "6";
- public override int Version { get; } = 1;
-
- public override bool NeedsAutomaticRootDependencyCalculation => true;
-
- ///
- protected override IList SkippedFolders => new List { "node_modules", "pnpm-store" };
-
- protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs)
+ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder)
{
- var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
- var file = processRequest.ComponentStream;
+ var yaml = PnpmParsingUtilities.DeserializePnpmYamlV6File(yamlFileContent);
- var skippedFolder = this.SkippedFolders.FirstOrDefault(folder => file.Location.Contains(folder));
- if (!string.IsNullOrEmpty(skippedFolder))
- {
- this.Logger.LogDebug("Skipping found file, it was detected as being within a {SkippedFolder} folder.", skippedFolder);
- }
-
- try
- {
- var fileContent = await new StreamReader(file.Stream).ReadToEndAsync();
- var version = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(fileContent);
- this.RecordLockfileVersion(version);
- var majorVersion = version?.Split(".")[0];
- switch (majorVersion)
- {
- case null:
- case "5":
- // Handled in the non-experimental detector. No-op here.
- break;
- case "6":
- var pnpmYamlV6 = PnpmParsingUtilities.DeserializePnpmYamlV6File(fileContent);
- this.RecordDependencyGraphFromFileV6(pnpmYamlV6, singleFileComponentRecorder);
- break;
- default:
- // Handled in the non-experimental detector. No-op here.
- break;
- }
- }
- catch (Exception e)
- {
- this.Logger.LogError(e, "Failed to read pnpm yaml file {File}", file.Location);
- }
- }
-
- private void RecordDependencyGraphFromFileV6(PnpmYamlV6 yaml, ISingleFileComponentRecorder singleFileComponentRecorder)
- {
// There may be multiple instance of the same package (even at the same version) in pnpm differentiated by other aspects of the pnpm dependency path.
// Therefor all DetectedComponents are tracked by the same full string pnpm uses, the pnpm dependency path, which is used as the key in this dictionary.
// Some documentation about pnpm dependency paths can be found at https://github.com/pnpm/spec/blob/master/dependency-path.md.
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs
deleted file mode 100644
index d8bb95354..000000000
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs
+++ /dev/null
@@ -1,128 +0,0 @@
-namespace Microsoft.ComponentDetection.Detectors.Pnpm;
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.ComponentDetection.Contracts;
-using Microsoft.ComponentDetection.Contracts.Internal;
-using Microsoft.ComponentDetection.Contracts.TypedComponent;
-using Microsoft.Extensions.Logging;
-
-public class PnpmComponentDetector : FileComponentDetector
-{
- public PnpmComponentDetector(
- IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
- IObservableDirectoryWalkerFactory walkerFactory,
- ILogger logger)
- {
- this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
- this.Scanner = walkerFactory;
- this.Logger = logger;
- }
-
- public override string Id { get; } = "Pnpm";
-
- public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) };
-
- public override IList SearchPatterns { get; } = new List { "shrinkwrap.yaml", "pnpm-lock.yaml" };
-
- public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm };
-
- public override int Version { get; } = 5;
-
- public override bool NeedsAutomaticRootDependencyCalculation => true;
-
- ///
- protected override IList SkippedFolders => new List { "node_modules", "pnpm-store" };
-
- protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs)
- {
- var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
- var file = processRequest.ComponentStream;
-
- this.Logger.LogDebug("Found yaml file: {YamlFile}", file.Location);
- var skippedFolder = this.SkippedFolders.FirstOrDefault(folder => file.Location.Contains(folder));
- if (!string.IsNullOrEmpty(skippedFolder))
- {
- this.Logger.LogDebug("Skipping found file, it was detected as being within a {SkippedFolder} folder.", skippedFolder);
- }
-
- try
- {
- var fileContent = await new StreamReader(file.Stream).ReadToEndAsync();
- var version = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(fileContent);
- this.RecordLockfileVersion(version);
- var majorVersion = version?.Split(".")[0];
- switch (majorVersion)
- {
- case null:
- // The null case falls through to version 5 to preserver the behavior of this scanner from before version specific logic was added.
- // This allows files versioned with "shrinkwrapVersion" (such as one included in some of the tests) to be used.
- // Given that "shrinkwrapVersion" is a concept from file format version 4 https://github.com/pnpm/spec/blob/master/lockfile/4.md)
- // this case might not be robust.
- case "5":
- var pnpmYamlV5 = PnpmParsingUtilities.DeserializePnpmYamlV5File(fileContent);
- this.RecordDependencyGraphFromFileV5(pnpmYamlV5, singleFileComponentRecorder);
- break;
- case "6":
- // Handled in the experimental detector. No-op here.
- break;
- default:
- this.Logger.LogWarning("Unsupported lockfileVersion in pnpm yaml file {File}", file.Location);
- break;
- }
- }
- catch (Exception e)
- {
- this.Logger.LogError(e, "Failed to read pnpm yaml file {File}", file.Location);
- }
- }
-
- private void RecordDependencyGraphFromFileV5(PnpmYamlV5 yaml, ISingleFileComponentRecorder singleFileComponentRecorder)
- {
- foreach (var packageKeyValue in yaml.Packages ?? Enumerable.Empty>())
- {
- // Ignore file: as these are local packages.
- if (packageKeyValue.Key.StartsWith("file:"))
- {
- continue;
- }
-
- var parentDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5(pnpmPackagePath: packageKeyValue.Key);
- var isDevDependency = packageKeyValue.Value != null && PnpmParsingUtilities.IsPnpmPackageDevDependency(packageKeyValue.Value);
- singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: isDevDependency);
- parentDetectedComponent = singleFileComponentRecorder.GetComponent(parentDetectedComponent.Component.Id);
-
- if (packageKeyValue.Value.Dependencies != null)
- {
- foreach (var dependency in packageKeyValue.Value.Dependencies)
- {
- // Ignore local packages.
- if (PnpmParsingUtilities.IsLocalDependency(dependency))
- {
- continue;
- }
-
- var childDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5(
- pnpmPackagePath: PnpmParsingUtilities.CreatePnpmPackagePathFromDependencyV5(dependency.Key, dependency.Value));
-
- // Older code used the root's dev dependency value. We're leaving this null until we do a second pass to look at each components' top level referrers.
- singleFileComponentRecorder.RegisterUsage(childDetectedComponent, parentComponentId: parentDetectedComponent.Component.Id, isDevelopmentDependency: null);
- }
- }
- }
-
- // PNPM doesn't know at the time of RegisterUsage being called for a dependency whether something is a dev dependency or not, so after building up the graph we look at top level referrers.
- foreach (var component in singleFileComponentRecorder.GetDetectedComponents())
- {
- var graph = singleFileComponentRecorder.DependencyGraph;
- var explicitReferences = graph.GetExplicitReferencedDependencyIds(component.Key);
- foreach (var explicitReference in explicitReferences)
- {
- singleFileComponentRecorder.RegisterUsage(component.Value, isDevelopmentDependency: graph.IsDevelopmentDependency(explicitReference));
- }
- }
- }
-}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs
new file mode 100644
index 000000000..32750124e
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs
@@ -0,0 +1,115 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.Internal;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.Extensions.Logging;
+
+///
+/// Factory responsible for constructing the proper and recording its dependency
+/// graph based on the file found during file component detection.
+///
+public class PnpmComponentDetectorFactory : FileComponentDetector
+{
+ ///
+ /// The maximum version of the report specification that this detector can handle.
+ ///
+ private static readonly Version MaxLockfileVersion = new(6, 0);
+
+ public PnpmComponentDetectorFactory(
+ IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
+ IObservableDirectoryWalkerFactory walkerFactory,
+ ILogger logger)
+ {
+ this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
+ this.Scanner = walkerFactory;
+ this.Logger = logger;
+ }
+
+ public override string Id { get; } = "Pnpm";
+
+ public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) };
+
+ public override IList SearchPatterns { get; } = new List { "shrinkwrap.yaml", "pnpm-lock.yaml" };
+
+ public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm };
+
+ public override int Version { get; } = 6;
+
+ public override bool NeedsAutomaticRootDependencyCalculation => true;
+
+ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs)
+ {
+ var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
+ var file = processRequest.ComponentStream;
+ this.Logger.LogDebug("Found yaml file: {YamlFile}", file.Location);
+ var skippedFolder = this.SkippedFolders.FirstOrDefault(folder => file.Location.Contains(folder));
+ if (!string.IsNullOrEmpty(skippedFolder))
+ {
+ this.Logger.LogDebug("Skipping found file, it was detected as being within a {SkippedFolder} folder.", skippedFolder);
+ }
+
+ try
+ {
+ var fileContent = await new StreamReader(file.Stream).ReadToEndAsync();
+ var detector = this.GetPnpmComponentDetector(fileContent, out var detectedVersion);
+ if (detector == null)
+ {
+ this.Logger.LogWarning("Unsupported lockfileVersion in pnpm yaml file {File}", file.Location);
+ using var unsupportedVersionRecord = new InvalidParseVersionTelemetryRecord
+ {
+ DetectorId = this.Id,
+ FilePath = file.Location,
+ Version = detectedVersion,
+ MaxVersion = MaxLockfileVersion.ToString(),
+ };
+ }
+ else
+ {
+ this.Logger.LogDebug(
+ "Found Pnmp yaml file '{Location}' with version '{Version}' so using PnpmDetector of type '{Type}'.",
+ file.Location,
+ detectedVersion ?? "null",
+ detector.GetType().Name);
+
+ detector.RecordDependencyGraphFromFile(fileContent, singleFileComponentRecorder);
+ }
+ }
+ catch (Exception e)
+ {
+ this.Logger.LogError(e, "Failed to read pnpm yaml file {File}", file.Location);
+
+ using var failedParsingRecord = new FailedParsingFileRecord
+ {
+ DetectorId = this.Id,
+ FilePath = file.Location,
+ ExceptionMessage = e.Message,
+ StackTrace = e.StackTrace,
+ };
+ }
+ }
+
+ private IPnpmDetector GetPnpmComponentDetector(string fileContent, out string detectedVersion)
+ {
+ detectedVersion = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(fileContent);
+ this.RecordLockfileVersion(detectedVersion);
+ var majorVersion = detectedVersion?.Split(".")[0];
+ return majorVersion switch
+ {
+ // The null case falls through to version 5 to preserve the behavior of this scanner from before version specific logic was added.
+ // This allows files versioned with "shrinkwrapVersion" (such as one included in some of the tests) to be used.
+ // Given that "shrinkwrapVersion" is a concept from file format version 4 https://github.com/pnpm/spec/blob/master/lockfile/4.md)
+ // this case might not be robust.
+ null => new Pnpm5Detector(),
+ Pnpm5Detector.MajorVersion => new Pnpm5Detector(),
+ Pnpm6Detector.MajorVersion => new Pnpm6Detector(),
+ _ => null,
+ };
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/Pnpm6Experiment.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/Pnpm6Experiment.cs
deleted file mode 100644
index 32f9b9b92..000000000
--- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/Pnpm6Experiment.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs;
-
-using Microsoft.ComponentDetection.Contracts;
-using Microsoft.ComponentDetection.Detectors.Pnpm;
-
-///
-/// Validating the .
-///
-public class Pnpm6Experiment : IExperimentConfiguration
-{
- public string Name => "Pnpm6";
-
- public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PnpmComponentDetector;
-
- public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is Pnpm6ComponentDetector;
-
- public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true;
-}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
index e819f515e..182ed884e 100644
--- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
@@ -64,7 +64,6 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddSingleton();
// Detectors
@@ -121,8 +120,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton();
// pnpm
- services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
// Poetry
services.AddSingleton();
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Pnpm6DetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/Pnpm6DetectorTests.cs
deleted file mode 100644
index 8381acf45..000000000
--- a/test/Microsoft.ComponentDetection.Detectors.Tests/Pnpm6DetectorTests.cs
+++ /dev/null
@@ -1,188 +0,0 @@
-namespace Microsoft.ComponentDetection.Detectors.Tests;
-
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using FluentAssertions;
-using Microsoft.ComponentDetection.Common.DependencyGraph;
-using Microsoft.ComponentDetection.Contracts;
-using Microsoft.ComponentDetection.Contracts.TypedComponent;
-using Microsoft.ComponentDetection.Detectors.Pnpm;
-using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
-using Microsoft.ComponentDetection.TestsUtilities;
-using Microsoft.Extensions.Logging;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using Moq;
-
-[TestClass]
-[TestCategory("Governance/All")]
-[TestCategory("Governance/ComponentDetection")]
-public class Pnpm6DetectorTests : BaseDetectorTest
-{
- public Pnpm6DetectorTests()
- {
- var componentRecorder = new ComponentRecorder(enableManualTrackingOfExplicitReferences: false);
- this.DetectorTestUtility.WithScanRequest(
- new ScanRequest(
- new DirectoryInfo(Path.GetTempPath()),
- null,
- null,
- new Dictionary(),
- null,
- componentRecorder));
- this.DetectorTestUtility.AddServiceMock(new Mock>());
- }
-
- [TestMethod]
- public async Task TestPnpmDetector_V6Async()
- {
- var yamlFile = @"
-lockfileVersion: '6.0'
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-dependencies:
- minimist:
- specifier: 1.2.8
- version: 1.2.8
-packages:
- /minimist@1.2.8:
- resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
- dev: false
-";
-
- var (scanResult, componentRecorder) = await this.DetectorTestUtility
- .WithFile("pnpm-lock.yaml", yamlFile)
- .ExecuteDetectorAsync();
-
- scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
-
- var detectedComponents = componentRecorder.GetDetectedComponents();
- detectedComponents.Should().ContainSingle();
-
- var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("minimist"));
- componentRecorder.AssertAllExplicitlyReferencedComponents(
- minimist.Component.Id,
- parentComponent => parentComponent.Name == "minimist");
-
- componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml"));
-
- foreach (var component in detectedComponents)
- {
- component.Component.Type.Should().Be(ComponentType.Npm);
- }
- }
-
- [TestMethod]
- public async Task TestPnpmDetector_V6WorkspaceAsync()
- {
- var yamlFile = @"
-lockfileVersion: '6.0'
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-importers:
- .:
- dependencies:
- minimist:
- specifier: 1.2.8
- version: 1.2.8
-packages:
- /minimist@1.2.8:
- resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
- dev: false
-";
-
- var (scanResult, componentRecorder) = await this.DetectorTestUtility
- .WithFile("pnpm-lock.yaml", yamlFile)
- .ExecuteDetectorAsync();
-
- scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
-
- var detectedComponents = componentRecorder.GetDetectedComponents();
- detectedComponents.Should().ContainSingle();
-
- var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("minimist"));
- componentRecorder.AssertAllExplicitlyReferencedComponents(
- minimist.Component.Id,
- parentComponent => parentComponent.Name == "minimist");
-
- componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml"));
-
- foreach (var component in detectedComponents)
- {
- component.Component.Type.Should().Be(ComponentType.Npm);
- }
- }
-
- // Test that renamed package is handled correctly, and that resolved version gets used (not specifier)
- [TestMethod]
- public async Task TestPnpmDetector_V6RenamedAsync()
- {
- var yamlFile = @"
-lockfileVersion: '6.0'
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-dependencies:
- renamed:
- specifier: npm:minimist@*
- version: /minimist@1.2.8
-packages:
- /minimist@1.2.8:
- resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
- dev: false
-";
-
- var (scanResult, componentRecorder) = await this.DetectorTestUtility
- .WithFile("pnpm-lock.yaml", yamlFile)
- .ExecuteDetectorAsync();
-
- scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
-
- var detectedComponents = componentRecorder.GetDetectedComponents();
- detectedComponents.Should().ContainSingle();
-
- var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Equals("minimist"));
- componentRecorder.AssertAllExplicitlyReferencedComponents(
- minimist.Component.Id,
- parentComponent => parentComponent.Name == "minimist");
- ((NpmComponent)minimist.Component).Version.Should().BeEquivalentTo("1.2.8");
-
- componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml"));
-
- foreach (var component in detectedComponents)
- {
- component.Component.Type.Should().Be(ComponentType.Npm);
- }
- }
-
- [TestMethod]
- public async Task TestPnpmDetector_V6_BadLockVersion_EmptyAsync()
- {
- var yamlFile = @"
-lockfileVersion: '5.0'
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-dependencies:
- renamed:
- specifier: npm:minimist@*
- version: /minimist@1.2.8
-packages:
- /minimist@1.2.8:
- resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
- dev: false
-";
-
- var (scanResult, componentRecorder) = await this.DetectorTestUtility
- .WithFile("pnpm-lock.yaml", yamlFile)
- .ExecuteDetectorAsync();
-
- scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
-
- var detectedComponents = componentRecorder.GetDetectedComponents();
- detectedComponents.Should().BeEmpty();
- }
-}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs
index 3b6b8e839..c43b019be 100644
--- a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs
@@ -18,7 +18,7 @@ namespace Microsoft.ComponentDetection.Detectors.Tests;
[TestClass]
[TestCategory("Governance/All")]
[TestCategory("Governance/ComponentDetection")]
-public class PnpmDetectorTests : BaseDetectorTest
+public class PnpmDetectorTests : BaseDetectorTest
{
public PnpmDetectorTests()
{
@@ -394,10 +394,190 @@ public async Task TestPnpmDetector_DependenciesRefeToLocalPaths_DependenciesAreI
}
[TestMethod]
- public async Task TestPnpmDetector_V5_BadLockVersion_EmptyAsync()
+ public async Task TestPnpmDetector_BadLockVersion_EmptyAsync()
+ {
+ var yamlFile = @"
+lockfileVersion: '4.0'
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+dependencies:
+ renamed:
+ specifier: npm:minimist@*
+ version: /minimist@1.2.8
+packages:
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: false
+";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility
+ .WithFile("pnpm-lock.yaml", yamlFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+ detectedComponents.Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task TestPnpmDetector_V5_GoodLockVersion_ParsedDependenciesAsync()
+ {
+ var yamlFile = @"
+lockfileVersion: '5.0'
+dependencies:
+ 'query-string': 4.3.4,
+ 'strict-uri-encode': 1.1.0
+packages:
+ /query-string/4.3.4:
+ dev: false
+ /strict-uri-encode/1.1.0:
+ dev: true";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility
+ .WithFile("pnpm-lock.yaml", yamlFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+ var noDevDependencyComponent = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }).FirstOrDefault(x => x.Component.Name.Contains("query-string"));
+ var devDependencyComponent = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }).FirstOrDefault(x => x.Component.Name.Contains("strict-uri-encode"));
+
+ componentRecorder.GetEffectiveDevDependencyValue(noDevDependencyComponent.Component.Id).Should().BeFalse();
+ componentRecorder.GetEffectiveDevDependencyValue(devDependencyComponent.Component.Id).Should().BeTrue();
+ }
+
+ [TestMethod]
+ public async Task TestPnpmDetector_V6_SuccessAsync()
{
var yamlFile = @"
lockfileVersion: '6.0'
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+dependencies:
+ minimist:
+ specifier: 1.2.8
+ version: 1.2.8
+packages:
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: false
+";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility
+ .WithFile("pnpm-lock.yaml", yamlFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+ detectedComponents.Should().ContainSingle();
+
+ var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("minimist"));
+ componentRecorder.AssertAllExplicitlyReferencedComponents(
+ minimist.Component.Id,
+ parentComponent => parentComponent.Name == "minimist");
+
+ componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml"));
+
+ foreach (var component in detectedComponents)
+ {
+ component.Component.Type.Should().Be(ComponentType.Npm);
+ }
+ }
+
+ [TestMethod]
+ public async Task TestPnpmDetector_V6_WorkspaceAsync()
+ {
+ var yamlFile = @"
+lockfileVersion: '6.0'
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+importers:
+ .:
+ dependencies:
+ minimist:
+ specifier: 1.2.8
+ version: 1.2.8
+packages:
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: false
+";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility
+ .WithFile("pnpm-lock.yaml", yamlFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+ detectedComponents.Should().ContainSingle();
+
+ var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("minimist"));
+ componentRecorder.AssertAllExplicitlyReferencedComponents(
+ minimist.Component.Id,
+ parentComponent => parentComponent.Name == "minimist");
+
+ componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml"));
+
+ foreach (var component in detectedComponents)
+ {
+ component.Component.Type.Should().Be(ComponentType.Npm);
+ }
+ }
+
+ // Test that renamed package is handled correctly, and that resolved version gets used (not specifier)
+ [TestMethod]
+ public async Task TestPnpmDetector_V6_RenamedAsync()
+ {
+ var yamlFile = @"
+lockfileVersion: '6.0'
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+dependencies:
+ renamed:
+ specifier: npm:minimist@*
+ version: /minimist@1.2.8
+packages:
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: false
+";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility
+ .WithFile("pnpm-lock.yaml", yamlFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+ detectedComponents.Should().ContainSingle();
+
+ var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Equals("minimist"));
+ componentRecorder.AssertAllExplicitlyReferencedComponents(
+ minimist.Component.Id,
+ parentComponent => parentComponent.Name == "minimist");
+ ((NpmComponent)minimist.Component).Version.Should().BeEquivalentTo("1.2.8");
+
+ componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml"));
+
+ foreach (var component in detectedComponents)
+ {
+ component.Component.Type.Should().Be(ComponentType.Npm);
+ }
+ }
+
+ [TestMethod]
+ public async Task TestPnpmDetector_V6_BadLockVersion_EmptyAsync()
+ {
+ var yamlFile = @"
+lockfileVersion: '5.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false