Skip to content

Commit

Permalink
Report TargetFrameworks as part of DetectedComponent class (#1297)
Browse files Browse the repository at this point in the history
* Report TargetFrameworks as part of DetectedComponent class
  • Loading branch information
grvillic authored Nov 3, 2024
1 parent c2546fa commit a55475d
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ public void RegisterUsage(
bool isExplicitReferencedDependency = false,
string parentComponentId = null,
bool? isDevelopmentDependency = null,
DependencyScope? dependencyScope = null)
DependencyScope? dependencyScope = null,
string targetFramework = null)
{
ArgumentNullException.ThrowIfNull(detectedComponent);

Expand All @@ -186,6 +187,12 @@ public void RegisterUsage(
lock (this.registerUsageLock)
{
storedComponent = this.detectedComponentsInternal.GetOrAdd(componentId, detectedComponent);

if (!string.IsNullOrWhiteSpace(targetFramework))
{
storedComponent.TargetFrameworks.Add(targetFramework.Trim());
}

this.AddComponentToGraph(this.ManifestFileLocation, detectedComponent, isExplicitReferencedDependency, parentComponentId, isDevelopmentDependency, dependencyScope);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ public class ScannedComponent
public IEnumerable<int> ContainerDetailIds { get; set; }

public IDictionary<int, IEnumerable<int>> ContainerLayerIds { get; set; }

public ISet<string> TargetFrameworks { get; set; }
}
63 changes: 63 additions & 0 deletions src/Microsoft.ComponentDetection.Contracts/ConcurrentHashSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace Microsoft.ComponentDetection.Contracts;

using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;

/// <summary>Represents a thread-safe set of values.</summary>
/// <typeparam name="T">The type of elements in the hash set.</typeparam>
public class ConcurrentHashSet<T> : IEnumerable<T>
{
private readonly ConcurrentDictionary<T, byte> dictionary;

// Create different constructors for different equality comparers
public ConcurrentHashSet() => this.dictionary = new ConcurrentDictionary<T, byte>();

public ConcurrentHashSet(IEqualityComparer<T> comparer) => this.dictionary = new ConcurrentDictionary<T, byte>(comparer);

/// <summary>Adds the specific element to the <see cref="ConcurrentHashSet{T}"/> object.</summary>
/// <param name="item">The element to add to the set.</param>
/// <returns>true if element was added to <see cref="ConcurrentHashSet{T}"/> object; false, if item was already present.</returns>
public bool Add(T item)
{
return this.dictionary.TryAdd(item, 0);
}

/// <summary>Removes the specific element to the <see cref="ConcurrentHashSet{T}"/> object.</summary>
/// <param name="item">The element to be removed from the set.</param>
/// <returns>true if element was successfully found and removed; otherwise, false.</returns>
public bool Remove(T item)
{
return this.dictionary.TryRemove(item, out _);
}

/// <summary>Determines whether the <see cref="ConcurrentHashSet{T}"/> contains the specified element.</summary>
/// <param name="item">The element to locate in the <see cref="ConcurrentHashSet{T}"/> object.</param>
/// <returns>true if the <see cref="ConcurrentHashSet{T}"/> object contains the specified element; otherwise, false.</returns>
public bool Contains(T item)
{
return this.dictionary.ContainsKey(item);
}

/// <summary>Removes all elements from a <see cref="ConcurrentHashSet{T}"/> object.</summary>
public void Clear() => this.dictionary.Clear();

public ISet<T> ToHashSet()
{
return new HashSet<T>(this.dictionary.Keys);
}

/// <summary>Returns an enumerator that iterates through the <see cref="ConcurrentHashSet{T}"/>.</summary>
/// <returns>An enumerator for the <see cref="ConcurrentHashSet{T}"/>.</returns>
public IEnumerator<T> GetEnumerator()
{
return this.dictionary.Keys.GetEnumerator();
}

/// <summary>Returns an enumerator that iterates through the <see cref="ConcurrentHashSet{T}"/>.</summary>
/// <returns>An enumerator for the <see cref="ConcurrentHashSet{T}"/>.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public DetectedComponent(TypedComponent.TypedComponent component, IComponentDete
this.DetectedBy = detector;
this.ContainerDetailIds = [];
this.ContainerLayerIds = new Dictionary<int, IEnumerable<int>>();
this.TargetFrameworks = [];

if (containerDetailsId.HasValue)
{
this.ContainerDetailIds.Add(containerDetailsId.Value);
Expand Down Expand Up @@ -62,6 +64,9 @@ public DetectedComponent(TypedComponent.TypedComponent component, IComponentDete
/// <summary> Gets or sets Dependency Scope of the component.</summary>
public DependencyScope? DependencyScope { get; set; }

/// <summary> Gets Target Frameworks where the component was consumed.</summary>
public ConcurrentHashSet<string> TargetFrameworks { get; set; }

private string DebuggerDisplay => $"{this.Component.DebuggerDisplay}";

/// <summary>Adds a filepath to the FilePaths hashset for this detected component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,22 @@ public interface ISingleFileComponentRecorder
IDependencyGraph DependencyGraph { get; }

/// <summary>
/// Add or Update a component. In case that a parent componentId is specified
/// an edge is created between those components in the dependency graph.
/// Add or Update a component. In case that a parent componentId is specified an edge is created between those components in the dependency graph.
/// Metadata provided to this method specifies how the component was consumed, not uniquely identifying details about the component itself.
/// </summary>
/// <param name="detectedComponent">Component to add.</param>
/// <param name="isExplicitReferencedDependency">The value define if the component was referenced manually by the user in the location where the scanning is taking place.</param>
/// <param name="parentComponentId">Id of the parent component.</param>
/// <param name="isDevelopmentDependency">Boolean value indicating whether or not a component is a development-time dependency. Null implies that the value is unknown.</param>
/// <param name="dependencyScope">Enum value indicating scope of the component. </param>
/// <param name="targetFramework">Optional value to determine the framework where the component was consumed.</param>
void RegisterUsage(
DetectedComponent detectedComponent,
bool isExplicitReferencedDependency = false,
string parentComponentId = null,
bool? isDevelopmentDependency = null,
DependencyScope? dependencyScope = null);
DependencyScope? dependencyScope = null,
string targetFramework = null);

/// <summary>
/// Register that a package was unable to be processed.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
namespace Microsoft.ComponentDetection.Contracts.TypedComponent;

using System.Collections.Generic;
using PackageUrl;

public class NuGetComponent : TypedComponent
Expand All @@ -23,8 +22,6 @@ public NuGetComponent(string name, string version, string[] authors = null)

public string[] Authors { get; set; }

public ISet<string> TargetFrameworks { get; set; } = new HashSet<string>();

public override ComponentType Type => ComponentType.NuGet;

public override string Id => $"{this.Name} {this.Version} - {this.Type}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ namespace Microsoft.ComponentDetection.Detectors.Ivy;
/// in the project's build.xml, or if they use any file inclusion mechanism, it will fail.
///
/// The file written out by the custom Ant task is a simple JSON file representing a series of calls to be made to
/// the <see cref="ISingleFileComponentRecorder.RegisterUsage(DetectedComponent, bool, string, bool?, DependencyScope?)"/> method.
/// the <see cref="ISingleFileComponentRecorder.RegisterUsage(DetectedComponent, bool, string, bool?, DependencyScope?, string)"/> method.
/// </remarks>
public class IvyDetector : FileComponentDetector, IExperimentalDetector
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,14 @@ private void NavigateAndRegister(
visited ??= [];

var libraryComponent = new DetectedComponent(new NuGetComponent(library.Name, library.Version.ToNormalizedString()));
singleFileComponentRecorder.RegisterUsage(libraryComponent, explicitlyReferencedComponentIds.Contains(libraryComponent.Component.Id), parentComponentId, isDevelopmentDependency: isFrameworkComponent || isDevelopmentDependency);

// get the actual component in case it already exists
libraryComponent = singleFileComponentRecorder.GetComponent(libraryComponent.Component.Id);

// Add framework information to the actual component
if (target.TargetFramework is not null)
{
((NuGetComponent)libraryComponent.Component).TargetFrameworks.Add(target.TargetFramework.GetShortFolderName());
}
// Possibly adding target framework to single file recorder
singleFileComponentRecorder.RegisterUsage(
libraryComponent,
explicitlyReferencedComponentIds.Contains(libraryComponent.Component.Id),
parentComponentId,
isDevelopmentDependency: isFrameworkComponent || isDevelopmentDependency,
targetFramework: target.TargetFramework?.GetShortFolderName());

foreach (var dependency in library.Dependencies)
{
Expand All @@ -127,11 +125,9 @@ private void NavigateAndRegister(
}

var targetLibrary = target.GetTargetLibrary(dependency.Id);
if (targetLibrary == null)
{
// We have to exclude this case -- it looks like a bug in project.assets.json, but there are project.assets.json files that don't have a dependency library in the libraries set.
}
else

// There are project.assets.json files that don't have a dependency library in the libraries set.
if (targetLibrary != null)
{
visited.Add(dependency.Id);
this.NavigateAndRegister(target, explicitlyReferencedComponentIds, singleFileComponentRecorder, targetLibrary, libraryComponent.Component.Id, frameworkPackages, visited);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,11 @@ protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDiction
detectedComponent,
true,
null,
targetFramework: package.TargetFramework?.GetShortFolderName(),
/* TODO: Is this really the same concept?
Docs for NuGet say packages.config development dependencies are just not persisted as dependencies in the package.
That is not same as excluding from the output directory / runtime. */
package.IsDevelopmentDependency);

// get the actual component in case it already exists
var libraryComponent = singleFileComponentRecorder.GetComponent(detectedComponent.Component.Id);

// Add framework information to the actual component
if (package.TargetFramework is not null)
{
((NuGetComponent)libraryComponent.Component).TargetFrameworks.Add(package.TargetFramework.GetShortFolderName());
}
isDevelopmentDependency: package.IsDevelopmentDependency);
}
}
catch (Exception e) when (e is PackagesConfigReaderException or XmlException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,7 @@ private void NavigateAndRegister(
visited ??= [];

var libraryComponent = new DetectedComponent(new NuGetComponent(library.Name, library.Version.ToNormalizedString()));
singleFileComponentRecorder.RegisterUsage(libraryComponent, explicitlyReferencedComponentIds.Contains(libraryComponent.Component.Id), parentComponentId);

// get the actual component in case it already exists
libraryComponent = singleFileComponentRecorder.GetComponent(libraryComponent.Component.Id);

// Add framework information to the actual component
if (target.TargetFramework is not null)
{
((NuGetComponent)libraryComponent.Component).TargetFrameworks.Add(target.TargetFramework.GetShortFolderName());
}
singleFileComponentRecorder.RegisterUsage(libraryComponent, explicitlyReferencedComponentIds.Contains(libraryComponent.Component.Id), parentComponentId, targetFramework: target.TargetFramework?.GetShortFolderName());

foreach (var dependency in library.Dependencies)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ public ScanResult GenerateScanResultFromProcessingResult(
};
}

private static ConcurrentHashSet<string> MergeTargetFrameworks(ConcurrentHashSet<string> left, ConcurrentHashSet<string> right)
{
if (left == null && right == null)
{
return [];
}

if (left == null)
{
return right;
}

if (right == null)
{
return left;
}

foreach (var targetFramework in right)
{
left.Add(targetFramework);
}

return left;
}

private void LogComponentScopeTelemetry(List<DetectedComponent> components)
{
using var record = new DetectedComponentScopeRecord();
Expand Down Expand Up @@ -182,6 +207,8 @@ private DetectedComponent MergeComponents(IEnumerable<DetectedComponent> enumera
firstComponent.ContainerDetailIds.Add(containerDetailId);
}
}

firstComponent.TargetFrameworks = MergeTargetFrameworks(firstComponent.TargetFrameworks, nextComponent.TargetFrameworks);
}

return firstComponent;
Expand Down Expand Up @@ -271,6 +298,7 @@ private ScannedComponent ConvertToContract(DetectedComponent component)
AncestralReferrers = component.AncestralDependencyRoots,
ContainerDetailIds = component.ContainerDetailIds,
ContainerLayerIds = component.ContainerLayerIds,
TargetFrameworks = component.TargetFrameworks?.ToHashSet(),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public async Task ScanDirectoryAsync_Base_2_2_VerificationAsync()
x.Component.Id,
y => y.Id == x.Component.Id));

foreach (var component in detectedComponents)
{
component.TargetFrameworks.Should().BeEquivalentTo(["netcoreapp2.2"]);
}

componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.Should().Contain(location => location.Contains("Loader.csproj")));
}

Expand Down Expand Up @@ -195,6 +200,11 @@ public async Task ScanDirectoryAsync_Base_3_1_VerificationAsync()
systemTextJson.Component.Id,
x => x.Name.Contains("Microsoft.Extensions.DependencyModel")).Should().BeTrue();

foreach (var component in detectedComponents)
{
component.TargetFrameworks.Should().BeEquivalentTo(["netcoreapp3.1"]);
}

componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.Should().Contain(location => location.Contains("ExtCore.WebApplication.csproj")));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ public async Task ScanDirectoryAsync_Base_2_2_VerificationAsync()

var detectedComponents = componentRecorder.GetDetectedComponents();

// Number of unique nodes in ProjectAssetsJson
Console.WriteLine(string.Join(",", detectedComponents.Select(x => x.Component.Id)));
detectedComponents.Should().HaveCount(22);

var nonDevComponents = detectedComponents.Where(c => !componentRecorder.GetEffectiveDevDependencyValue(c.Component.Id).GetValueOrDefault());
nonDevComponents.Should().HaveCount(3);

foreach (var component in detectedComponents)
{
component.TargetFrameworks.Should().BeEquivalentTo(["netcoreapp2.2"]);
}

detectedComponents.Select(x => x.Component).Cast<NuGetComponent>().FirstOrDefault(x => x.Name.Contains("coverlet.msbuild")).Should().NotBeNull();

componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.Should().Contain(location => location.Contains("Loader.csproj")));
Expand All @@ -62,8 +65,6 @@ public async Task ScanDirectoryAsync_Base_2_2_additional_VerificationAsync()

var detectedComponents = componentRecorder.GetDetectedComponents();

// Number of unique nodes in ProjectAssetsJson
Console.WriteLine(string.Join(",", detectedComponents.Select(x => x.Component.Id)));
detectedComponents.Should().HaveCount(68);

var nonDevComponents = detectedComponents.Where(c => !componentRecorder.GetEffectiveDevDependencyValue(c.Component.Id).GetValueOrDefault());
Expand Down Expand Up @@ -198,6 +199,11 @@ public async Task ScanDirectoryAsync_Base_3_1_VerificationAsync()
systemTextJson.Component.Id,
x => x.Name.Contains("Microsoft.Extensions.DependencyModel")).Should().BeTrue();

foreach (var component in detectedComponents)
{
component.TargetFrameworks.Should().BeEquivalentTo(["netcoreapp3.1"]);
}

componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.Should().Contain(location => location.Contains("ExtCore.WebApplication.csproj")));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ public class NuGetPackagesConfigDetectorTests : BaseDetectorTest<NuGetPackagesCo
[TestMethod]
public async Task Should_WorkAsync()
{
var targetFramework = "net46";
var packagesConfig =
@"<?xml version=""1.0"" encoding=""utf-8""?>
@$"<?xml version=""1.0"" encoding=""utf-8""?>
<packages>
<package id=""jQuery"" version=""3.1.1"" targetFramework=""net46"" />
<package id=""NLog"" version=""4.3.10"" targetFramework=""net46"" />
<package id=""jQuery"" version=""3.1.1"" targetFramework=""{targetFramework}"" />
<package id=""NLog"" version=""4.3.10"" targetFramework=""{targetFramework}"" />
</packages>";

var (scanResult, componentRecorder) = await this.DetectorTestUtility
Expand All @@ -27,10 +28,17 @@ public async Task Should_WorkAsync()

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
var detectedComponents = componentRecorder.GetDetectedComponents();

var jqueryDetectedComponent = new DetectedComponent(new NuGetComponent("jQuery", "3.1.1"));
jqueryDetectedComponent.TargetFrameworks.Add(targetFramework);

var nlogDetectedComponent = new DetectedComponent(new NuGetComponent("NLog", "4.3.10"));
nlogDetectedComponent.TargetFrameworks.Add(targetFramework);

detectedComponents.Should().NotBeEmpty()
.And.HaveCount(2)
.And.ContainEquivalentOf(new DetectedComponent(new NuGetComponent("jQuery", "3.1.1")))
.And.ContainEquivalentOf(new DetectedComponent(new NuGetComponent("NLog", "4.3.10")));
.And.ContainEquivalentOf(jqueryDetectedComponent)
.And.ContainEquivalentOf(nlogDetectedComponent);
}

[TestMethod]
Expand Down

0 comments on commit a55475d

Please sign in to comment.