Skip to content

Commit 3f0be47

Browse files
authored
Merge pull request #14655 from michaelnebel/csharp/projectassetspackages
C#: Use `project.assets.json` for package dependencies.
2 parents 49428c4 + 4bcf9e5 commit 3f0be47

File tree

12 files changed

+632
-195
lines changed

12 files changed

+632
-195
lines changed

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/AssemblyCache.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ public AssemblyCache(IEnumerable<string> paths, ProgressMonitor progressMonitor)
2727
if (File.Exists(path))
2828
{
2929
pendingDllsToIndex.Enqueue(path);
30+
continue;
3031
}
31-
else
32+
33+
if (Directory.Exists(path))
3234
{
3335
progressMonitor.FindingFiles(path);
3436
AddReferenceDirectory(path);
3537
}
38+
else
39+
{
40+
progressMonitor.LogInfo("AssemblyCache: Path not found: " + path);
41+
}
3642
}
3743
IndexReferences();
3844
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using Newtonsoft.Json.Linq;
6+
using Semmle.Util;
7+
8+
namespace Semmle.Extraction.CSharp.DependencyFetching
9+
{
10+
/// <summary>
11+
/// Class for parsing project.assets.json files.
12+
/// </summary>
13+
internal class Assets
14+
{
15+
private readonly ProgressMonitor progressMonitor;
16+
17+
private static readonly string[] netFrameworks = new[] {
18+
"microsoft.aspnetcore.app.ref",
19+
"microsoft.netcore.app.ref",
20+
"microsoft.netframework.referenceassemblies",
21+
"microsoft.windowsdesktop.app.ref",
22+
"netstandard.library.ref"
23+
};
24+
25+
internal Assets(ProgressMonitor progressMonitor)
26+
{
27+
this.progressMonitor = progressMonitor;
28+
}
29+
30+
/// <summary>
31+
/// Class needed for deserializing parts of an assets file.
32+
/// It holds information about a reference.
33+
///
34+
/// Type carries the type of the reference.
35+
/// We are only interested in package references.
36+
///
37+
/// Compile holds information about the files needed for compilation.
38+
/// However, if it is a .NET framework reference we assume that all files in the
39+
/// package are needed for compilation.
40+
/// </summary>
41+
private record class ReferenceInfo(string? Type, Dictionary<string, object>? Compile);
42+
43+
/// <summary>
44+
/// Add the package dependencies from the assets file to dependencies.
45+
///
46+
/// Parse a part of the JSon assets file and add the paths
47+
/// to the dependencies required for compilation (and collect
48+
/// information about used packages).
49+
///
50+
/// Example:
51+
/// {
52+
/// "Castle.Core/4.4.1": {
53+
/// "type": "package",
54+
/// "compile": {
55+
/// "lib/netstandard1.5/Castle.Core.dll": {
56+
/// "related": ".xml"
57+
/// }
58+
/// }
59+
/// },
60+
/// "Json.Net/1.0.33": {
61+
/// "type": "package",
62+
/// "compile": {
63+
/// "lib/netstandard2.0/Json.Net.dll": {}
64+
/// },
65+
/// "runtime": {
66+
/// "lib/netstandard2.0/Json.Net.dll": {}
67+
/// }
68+
/// }
69+
/// }
70+
///
71+
/// Returns dependencies
72+
/// RequiredPaths = {
73+
/// "castle.core/4.4.1/lib/netstandard1.5/Castle.Core.dll",
74+
/// "json.net/1.0.33/lib/netstandard2.0/Json.Net.dll"
75+
/// }
76+
/// UsedPackages = {
77+
/// "castle.core",
78+
/// "json.net"
79+
/// }
80+
/// </summary>
81+
private DependencyContainer AddPackageDependencies(JObject json, DependencyContainer dependencies)
82+
{
83+
// If there are more than one framework we need to pick just one.
84+
// To ensure stability we pick one based on the lexicographic order of
85+
// the framework names.
86+
var references = json
87+
.GetProperty("targets")?
88+
.Properties()?
89+
.MaxBy(p => p.Name)?
90+
.Value
91+
.ToObject<Dictionary<string, ReferenceInfo>>();
92+
93+
if (references is null)
94+
{
95+
progressMonitor.LogDebug("No references found in the targets section in the assets file.");
96+
return dependencies;
97+
}
98+
99+
// Find all the compile dependencies for each reference and
100+
// create the relative path to the dependency.
101+
references
102+
.ForEach(r =>
103+
{
104+
var info = r.Value;
105+
var name = r.Key.ToLowerInvariant();
106+
if (info.Type != "package")
107+
{
108+
return;
109+
}
110+
111+
// If this is a .NET framework reference then include everything.
112+
if (netFrameworks.Any(framework => name.StartsWith(framework)))
113+
{
114+
dependencies.Add(name);
115+
}
116+
else
117+
{
118+
info.Compile?
119+
.ForEach(r => dependencies.Add(name, r.Key));
120+
}
121+
});
122+
123+
return dependencies;
124+
}
125+
126+
/// <summary>
127+
/// Parse `json` as project.assets.json content and add relative paths to the dependencies
128+
/// (together with used package information) required for compilation.
129+
/// </summary>
130+
/// <returns>True if parsing succeeds, otherwise false.</returns>
131+
public bool TryParse(string json, DependencyContainer dependencies)
132+
{
133+
try
134+
{
135+
var obj = JObject.Parse(json);
136+
AddPackageDependencies(obj, dependencies);
137+
return true;
138+
}
139+
catch (Exception e)
140+
{
141+
progressMonitor.LogDebug($"Failed to parse assets file (unexpected error): {e.Message}");
142+
return false;
143+
}
144+
}
145+
146+
public static DependencyContainer GetCompilationDependencies(ProgressMonitor progressMonitor, IEnumerable<string> assets)
147+
{
148+
var parser = new Assets(progressMonitor);
149+
var dependencies = new DependencyContainer();
150+
assets.ForEach(asset =>
151+
{
152+
var json = File.ReadAllText(asset);
153+
parser.TryParse(json, dependencies);
154+
});
155+
return dependencies;
156+
}
157+
}
158+
159+
internal static class JsonExtensions
160+
{
161+
internal static JObject? GetProperty(this JObject json, string property) =>
162+
json[property] as JObject;
163+
}
164+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Linq;
4+
5+
namespace Semmle.Extraction.CSharp.DependencyFetching
6+
{
7+
/// <summary>
8+
/// Container class for dependencies found in the assets file.
9+
/// </summary>
10+
internal class DependencyContainer
11+
{
12+
private readonly List<string> requiredPaths = new();
13+
private readonly HashSet<string> usedPackages = new();
14+
15+
/// <summary>
16+
/// In most cases paths in asset files point to dll's or the empty _._ file, which
17+
/// is sometimes there to avoid the directory being empty.
18+
/// That is, if the path specifically adds a .dll we use that, otherwise we as a fallback
19+
/// add the entire directory (which should be fine in case of _._ as well).
20+
/// </summary>
21+
private static string ParseFilePath(string path)
22+
{
23+
if (path.EndsWith(".dll"))
24+
{
25+
return path;
26+
}
27+
return Path.GetDirectoryName(path) ?? path;
28+
}
29+
30+
private static string GetPackageName(string package) =>
31+
package
32+
.Split(Path.DirectorySeparatorChar)
33+
.First();
34+
35+
/// <summary>
36+
/// Paths to dependencies required for compilation.
37+
/// </summary>
38+
public IEnumerable<string> RequiredPaths => requiredPaths;
39+
40+
/// <summary>
41+
/// Packages that are used as a part of the required dependencies.
42+
/// </summary>
43+
public HashSet<string> UsedPackages => usedPackages;
44+
45+
/// <summary>
46+
/// Add a dependency inside a package.
47+
/// </summary>
48+
public void Add(string package, string dependency)
49+
{
50+
var p = package.Replace('/', Path.DirectorySeparatorChar);
51+
var d = dependency.Replace('/', Path.DirectorySeparatorChar);
52+
53+
var path = Path.Combine(p, ParseFilePath(d));
54+
requiredPaths.Add(path);
55+
usedPackages.Add(GetPackageName(p));
56+
}
57+
58+
/// <summary>
59+
/// Add a dependency to an entire package
60+
/// </summary>
61+
public void Add(string package)
62+
{
63+
var p = package.Replace('/', Path.DirectorySeparatorChar);
64+
65+
requiredPaths.Add(p);
66+
usedPackages.Add(GetPackageName(p));
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)