Skip to content

Commit 9e988bb

Browse files
siyadava-sindhuSindhusha Yadavalli
andauthored
Updating "ComponentFilePaths" list of Yarn's Workspace Dependencies. (#565)
* Adding PackageJson LocationPath In YarnDetector * Adding new tests for YarnDetector to check workspace filepath addition * Update YarnLockDetectorTests.cs Addressing iteration-2 comments, and using FluentAssertions * Added 'GetLocationMapKey' util & updated YarnLockDetectorTests's FilePath logic to check Package.json entry existence alone. * Updating YarnDetectorTests to check filepaths count * Updating YarnLockcomponentDetector version --------- Co-authored-by: Sindhusha Yadavalli <Sindhusha.Yadavalli@microsoft.com>
1 parent 8874da6 commit 9e988bb

File tree

6 files changed

+137
-24
lines changed

6 files changed

+137
-24
lines changed

src/Microsoft.ComponentDetection.Common/DependencyGraph/ComponentRecorder.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,6 @@ public void RegisterUsage(
173173
}
174174

175175
#if DEBUG
176-
if (detectedComponent.FilePaths?.Any() ?? false)
177-
{
178-
this.logger.LogWarning("Detector should not populate DetectedComponent.FilePaths!");
179-
}
180-
181176
if (detectedComponent.DependencyRoots?.Any() ?? false)
182177
{
183178
this.logger.LogWarning("Detector should not populate DetectedComponent.DependencyRoots!");

src/Microsoft.ComponentDetection.Contracts/DetectedComponent.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ public DetectedComponent(TypedComponent.TypedComponent component, IComponentDete
6161
private string DebuggerDisplay => $"{this.Component.DebuggerDisplay}";
6262

6363
/// <summary>Adds a filepath to the FilePaths hashset for this detected component.
64-
/// Note: Dependency Graph automatically captures the location where a component is found, no need to call it at all inside package manager detectors.</summary>
64+
/// Note:
65+
/// (1) Dependency Graph automatically captures the location where a component is found, no need to call it at all inside package manager detectors.</summary>
66+
/// (2) Only usecase where Detectors allowed to call this API is, in scenarios where "Detectors(eg:Yarn) further process other config files to get workspace dependencies recursively"
67+
/// and detectors need to add WorkspaceDependency found path too.
6568
/// <param name="filePath">The file path to add to the hashset.</param>
6669
public void AddComponentFilePath(string filePath)
6770
{

src/Microsoft.ComponentDetection.Detectors/yarn/YarnEntry.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Microsoft.ComponentDetection.Detectors.Yarn;
1+
namespace Microsoft.ComponentDetection.Detectors.Yarn;
22
using System.Collections.Generic;
33

44
public class YarnEntry
@@ -39,4 +39,9 @@ public class YarnEntry
3939
/// Gets or sets a value indicating whether or not the component is a dev dependency.
4040
/// </summary>
4141
public bool DevDependency { get; set; }
42+
43+
/// <summary>
44+
/// Gets or Sets the location for this yarnentry. Often a file path if not in test circumstances.
45+
/// </summary>
46+
public string Location { get; set; }
4247
}

src/Microsoft.ComponentDetection.Detectors/yarn/YarnLockComponentDetector.cs

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Microsoft.ComponentDetection.Detectors.Yarn;
1+
namespace Microsoft.ComponentDetection.Detectors.Yarn;
22

33
using System;
44
using System.Collections.Generic;
@@ -35,7 +35,7 @@ public YarnLockComponentDetector(
3535

3636
public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = new[] { ComponentType.Npm };
3737

38-
public override int Version => 6;
38+
public override int Version => 7;
3939

4040
public override IEnumerable<string> Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) };
4141

@@ -98,6 +98,12 @@ private void DetectComponents(YarnLockFile file, string location, ISingleFileCom
9898
foreach (var dependency in yarnRoots)
9999
{
100100
var root = new DetectedComponent(new NpmComponent(dependency.Name, dependency.Version));
101+
102+
if (!string.IsNullOrWhiteSpace(dependency.Location))
103+
{
104+
root.AddComponentFilePath(dependency.Location);
105+
}
106+
101107
this.AddDetectedComponentToGraph(root, null, singleFileComponentRecorder, isRootComponent: true);
102108
}
103109

@@ -207,9 +213,10 @@ private bool TryReadPeerPackageJsonRequestsAsYarnEntries(ISingleFileComponentRec
207213
return false;
208214
}
209215

216+
var workspaceDependencyVsLocationMap = new Dictionary<string, string>();
210217
if (yarnWorkspaces.Count > 0)
211218
{
212-
this.GetWorkspaceDependencies(yarnWorkspaces, new FileInfo(location).Directory, combinedDependencies);
219+
this.GetWorkspaceDependencies(yarnWorkspaces, new FileInfo(location).Directory, combinedDependencies, workspaceDependencyVsLocationMap);
213220
}
214221

215222
// Convert all of the dependencies we retrieved from package.json
@@ -232,13 +239,19 @@ private bool TryReadPeerPackageJsonRequestsAsYarnEntries(ISingleFileComponentRec
232239
entry.DevDependency = version.Value;
233240

234241
yarnRoots.Add(entry);
242+
243+
var locationMapDictonaryKey = this.GetLocationMapKey(name, version.Key);
244+
if (workspaceDependencyVsLocationMap.ContainsKey(locationMapDictonaryKey))
245+
{
246+
entry.Location = workspaceDependencyVsLocationMap[locationMapDictonaryKey];
247+
}
235248
}
236249
}
237250

238251
return true;
239252
}
240253

241-
private void GetWorkspaceDependencies(IList<string> yarnWorkspaces, DirectoryInfo root, IDictionary<string, IDictionary<string, bool>> dependencies)
254+
private void GetWorkspaceDependencies(IList<string> yarnWorkspaces, DirectoryInfo root, IDictionary<string, IDictionary<string, bool>> dependencies, IDictionary<string, string> workspaceDependencyVsLocationMap)
242255
{
243256
var ignoreCase = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
244257

@@ -263,31 +276,61 @@ private void GetWorkspaceDependencies(IList<string> yarnWorkspaces, DirectoryInf
263276

264277
foreach (var dependency in combinedDependencies)
265278
{
266-
this.ProcessWorkspaceDependency(dependencies, dependency);
279+
this.ProcessWorkspaceDependency(dependencies, dependency, workspaceDependencyVsLocationMap, stream.Location);
267280
}
268281
}
269282
}
270283
}
271284

272-
private void ProcessWorkspaceDependency(IDictionary<string, IDictionary<string, bool>> dependencies, KeyValuePair<string, IDictionary<string, bool>> newDependency)
285+
private void ProcessWorkspaceDependency(IDictionary<string, IDictionary<string, bool>> dependencies, KeyValuePair<string, IDictionary<string, bool>> newDependency, IDictionary<string, string> workspaceDependencyVsLocationMap, string streamLocation)
273286
{
274-
if (!dependencies.TryGetValue(newDependency.Key, out var existingDependency))
275-
{
276-
dependencies.Add(newDependency.Key, newDependency.Value);
277-
return;
278-
}
279-
280-
foreach (var item in newDependency.Value)
287+
try
281288
{
282-
if (existingDependency.TryGetValue(item.Key, out var wasDev))
289+
if (!dependencies.TryGetValue(newDependency.Key, out var existingDependency))
283290
{
284-
existingDependency[item.Key] = wasDev && item.Value;
291+
dependencies.Add(newDependency.Key, newDependency.Value);
292+
foreach (var item in newDependency.Value)
293+
{
294+
// Adding 'Package.json stream's location'(in which workspacedependency of Yarn.lock file was found) as location of respective WorkSpaceDependency.
295+
this.AddLocationInfoToWorkspaceDependency(workspaceDependencyVsLocationMap, streamLocation, newDependency.Key, item.Key);
296+
}
297+
298+
return;
285299
}
286-
else
300+
301+
foreach (var item in newDependency.Value)
287302
{
288-
existingDependency[item.Key] = item.Value;
303+
if (existingDependency.TryGetValue(item.Key, out var wasDev))
304+
{
305+
existingDependency[item.Key] = wasDev && item.Value;
306+
}
307+
else
308+
{
309+
existingDependency[item.Key] = item.Value;
310+
}
311+
312+
// Adding 'Package.json stream's location'(in which workspacedependency of Yarn.lock file was found) as location of respective WorkSpaceDependency.
313+
this.AddLocationInfoToWorkspaceDependency(workspaceDependencyVsLocationMap, streamLocation, newDependency.Key, item.Key);
289314
}
290315
}
316+
catch (Exception ex)
317+
{
318+
this.Logger.LogError(ex, "Could not process workspace dependency from file {PackageJsonStreamLocation}.", streamLocation);
319+
}
320+
}
321+
322+
private void AddLocationInfoToWorkspaceDependency(IDictionary<string, string> workspaceDependencyVsLocationMap, string streamLocation, string dependencyName, string dependencyVersion)
323+
{
324+
var locationMapDictionaryKey = this.GetLocationMapKey(dependencyName, dependencyVersion);
325+
if (!workspaceDependencyVsLocationMap.ContainsKey(locationMapDictionaryKey))
326+
{
327+
workspaceDependencyVsLocationMap[locationMapDictionaryKey] = streamLocation;
328+
}
329+
}
330+
331+
private string GetLocationMapKey(string dependencyName, string dependencyVersion)
332+
{
333+
return $"{dependencyName}-{dependencyVersion}";
291334
}
292335

293336
private void AddDetectedComponentToGraph(DetectedComponent componentToAdd, DetectedComponent parentComponent, ISingleFileComponentRecorder singleFileComponentRecorder, bool isRootComponent = false, bool? isDevDependency = null)

test/Microsoft.ComponentDetection.Detectors.Tests/YarnLockDetectorTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,52 @@ public async Task WellFormedYarnLockV1WithWorkspace_FindsComponentAsync()
240240
parentComponent => parentComponent.Name == componentA.Name && parentComponent.Version == version0);
241241
}
242242

243+
[TestMethod]
244+
public async Task WellFormedYarnLockV1WithWorkspace_CheckFilePathsAsync()
245+
{
246+
var directory = new DirectoryInfo(Path.GetTempPath());
247+
248+
var version0 = NewRandomVersion();
249+
var componentA = new YarnTestComponentDefinition
250+
{
251+
ActualVersion = version0,
252+
RequestedVersion = $"^{version0}",
253+
ResolvedVersion = "https://resolved0/a/resolved",
254+
Name = Guid.NewGuid().ToString("N"),
255+
};
256+
257+
var componentStream = YarnTestUtilities.GetMockedYarnLockStream("yarn.lock", this.CreateYarnLockV1FileContent(new List<YarnTestComponentDefinition> { componentA }));
258+
259+
var workspaceJson = new
260+
{
261+
name = "testworkspace",
262+
version = "1.0.0",
263+
@private = true,
264+
workspaces = new[] { "workspace" },
265+
};
266+
var str = JsonConvert.SerializeObject(workspaceJson);
267+
var workspaceJsonComponentStream = new ComponentStream { Location = directory.ToString(), Pattern = "package.json", Stream = str.ToStream() };
268+
269+
var packageStream = NpmTestUtilities.GetPackageJsonOneRootComponentStream(componentA.Name, componentA.RequestedVersion);
270+
271+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
272+
.WithFile("yarn.lock", componentStream.Stream)
273+
.WithFile("package.json", workspaceJsonComponentStream.Stream, new[] { "package.json" }, Path.Combine(Path.GetTempPath(), "package.json"))
274+
.WithFile("package.json", packageStream.Stream, new[] { "package.json" }, Path.Combine(Path.GetTempPath(), "workspace", "package.json"))
275+
.ExecuteDetectorAsync();
276+
277+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
278+
279+
var detectedComponents = componentRecorder.GetDetectedComponents();
280+
detectedComponents.Should().HaveCount(1);
281+
282+
// checking if workspace's "package.json FilePath entry" is added or not.
283+
var detectedFilePaths = detectedComponents.First().FilePaths;
284+
detectedFilePaths.Should().HaveCount(1);
285+
var expectedWorkSpacePackageJsonPath = Path.Combine(Path.GetTempPath(), "workspace", "package.json");
286+
detectedComponents.First().FilePaths.Contains(expectedWorkSpacePackageJsonPath).Should().Be(true);
287+
}
288+
243289
[TestMethod]
244290
public async Task WellFormedYarnLockV2WithWorkspace_FindsComponentAsync()
245291
{

test/Microsoft.ComponentDetection.TestsUtilities/DetectorTestUtilityBuilder.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace Microsoft.ComponentDetection.TestsUtilities;
22

3+
using System;
34
using System.Collections.Generic;
45
using System.IO;
56
using System.Linq;
@@ -200,6 +201,26 @@ private void InitializeFileMocks()
200201
fileToSend.Location,
201202
fileToSend.Contents)).Select(pr => pr.ComponentStream);
202203
});
204+
205+
this.mockComponentStreamEnumerableFactory.Setup(x =>
206+
x.GetComponentStreams(
207+
It.IsAny<DirectoryInfo>(),
208+
It.IsAny<Func<FileInfo, bool>>(),
209+
It.IsAny<ExcludeDirectoryPredicate>(),
210+
It.IsAny<bool>()))
211+
.Returns<DirectoryInfo, Func<FileInfo, bool>, ExcludeDirectoryPredicate, bool>(
212+
(directoryInfo, fileMatchingPredicate, _, recurse) =>
213+
{
214+
return filesToSend
215+
.Where(fileToSend => fileMatchingPredicate(new FileInfo(fileToSend.Location)))
216+
.Select(fileToSend =>
217+
this.CreateProcessRequest(
218+
FindMatchingPattern(
219+
fileToSend.Name,
220+
searchPatterns),
221+
fileToSend.Location,
222+
fileToSend.Contents)).Select(pr => pr.ComponentStream);
223+
});
203224
}
204225
}
205226
}

0 commit comments

Comments
 (0)