Skip to content

Commit 8931bed

Browse files
Handle version ranges (#8644)
* handle version ranges * Add Tests * Update tests
1 parent b03d16d commit 8931bed

4 files changed

Lines changed: 240 additions & 148 deletions

File tree

tools/apiview/parsers/csharp-api-parser/CSharpAPIParser/Program.cs

Lines changed: 184 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -10,195 +10,231 @@
1010
using NuGet.Protocol.Core.Types;
1111
using NuGet.Versioning;
1212

13-
var inputOption = new Option<FileInfo>("--packageFilePath", "C# Package (.nupkg) file").ExistingOnly();
14-
inputOption.IsRequired = true;
1513

16-
var outputOption1 = new Option<DirectoryInfo>("--outputDirectoryPath", "Directory for the output Token File").ExistingOnly();
17-
var outputOption2 = new Option<string>("--outputFileName", "Output File Name");
18-
var runAnalysis = new Argument<bool>("runAnalysis", "Run Analysis on the package");
19-
runAnalysis.SetDefaultValue(true);
20-
21-
var rootCommand = new RootCommand("Parse C# Package (.nupkg) to APIView Tokens")
22-
{
23-
inputOption,
24-
outputOption1,
25-
outputOption2,
26-
runAnalysis
27-
};
28-
29-
rootCommand.SetHandler(async (FileInfo packageFilePath, DirectoryInfo outputDirectory, string outputFileName, bool runAnalysis) =>
14+
public static class Program
3015
{
31-
try
16+
public static int Main(string[] args)
3217
{
33-
using (var stream = packageFilePath.OpenRead())
18+
var inputOption = new Option<FileInfo>("--packageFilePath", "C# Package (.nupkg) file").ExistingOnly();
19+
inputOption.IsRequired = true;
20+
21+
var outputOption1 = new Option<DirectoryInfo>("--outputDirectoryPath", "Directory for the output Token File").ExistingOnly();
22+
var outputOption2 = new Option<string>("--outputFileName", "Output File Name");
23+
var runAnalysis = new Argument<bool>("runAnalysis", "Run Analysis on the package");
24+
runAnalysis.SetDefaultValue(true);
25+
26+
var rootCommand = new RootCommand("Parse C# Package (.nupkg) to APIView Tokens")
3427
{
35-
await HandlePackageFileParsing(stream, packageFilePath, outputDirectory, outputFileName, runAnalysis);
36-
}
37-
}
38-
catch (Exception ex)
39-
{
40-
Console.Error.WriteLine($"Error Reading PackageFile : {ex.Message}");
41-
}
42-
}, inputOption, outputOption1,outputOption2, runAnalysis);
28+
inputOption,
29+
outputOption1,
30+
outputOption2,
31+
runAnalysis
32+
};
4333

44-
return rootCommand.InvokeAsync(args).Result;
34+
rootCommand.SetHandler(async (FileInfo packageFilePath, DirectoryInfo outputDirectory, string outputFileName, bool runAnalysis) =>
35+
{
36+
try
37+
{
38+
using (var stream = packageFilePath.OpenRead())
39+
{
40+
await HandlePackageFileParsing(stream, packageFilePath, outputDirectory, outputFileName, runAnalysis);
41+
}
42+
}
43+
catch (Exception ex)
44+
{
45+
Console.Error.WriteLine($"Error Reading PackageFile : {ex.Message}");
46+
}
47+
}, inputOption, outputOption1, outputOption2, runAnalysis);
4548

49+
return rootCommand.InvokeAsync(args).Result;
50+
}
4651

47-
static async Task HandlePackageFileParsing(Stream stream, FileInfo packageFilePath, DirectoryInfo OutputDirectory, string outputFileName, bool runAnalysis)
48-
{
49-
ZipArchive? zipArchive = null;
50-
Stream? dllStream = stream;
51-
Stream? docStream = null;
52-
List<DependencyInfo>? dependencies = new List<DependencyInfo>();
53-
string? dependencyFilesTempDir = null;
5452

55-
try
53+
static async Task HandlePackageFileParsing(Stream stream, FileInfo packageFilePath, DirectoryInfo OutputDirectory, string outputFileName, bool runAnalysis)
5654
{
57-
if (IsNuget(packageFilePath.FullName))
58-
{
59-
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
60-
var nuspecEntry = zipArchive.Entries.Single(entry => IsNuspec(entry.Name));
61-
var dllEntries = zipArchive.Entries.Where(entry => IsDll(entry.Name)).ToArray();
55+
ZipArchive? zipArchive = null;
56+
Stream? dllStream = stream;
57+
Stream? docStream = null;
58+
List<DependencyInfo>? dependencies = new List<DependencyInfo>();
59+
string? dependencyFilesTempDir = null;
6260

63-
if (dllEntries.Length == 0)
61+
try
62+
{
63+
if (IsNuget(packageFilePath.FullName))
6464
{
65-
Console.Error.WriteLine($"PackageFile {packageFilePath.FullName} contains no dlls.");
66-
return;
65+
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
66+
var nuspecEntry = zipArchive.Entries.Single(entry => IsNuspec(entry.Name));
67+
var dllEntries = zipArchive.Entries.Where(entry => IsDll(entry.Name)).ToArray();
68+
69+
if (dllEntries.Length == 0)
70+
{
71+
Console.Error.WriteLine($"PackageFile {packageFilePath.FullName} contains no dlls.");
72+
return;
73+
}
74+
75+
var dllEntry = dllEntries.First();
76+
if (dllEntries.Length > 1)
77+
{
78+
// If there are multiple dlls in the nupkg (e.g. Cosmos), try to find the first that matches the nuspec name, but
79+
// fallback to just using the first one.
80+
dllEntry = dllEntries.FirstOrDefault(
81+
dll => Path.GetFileNameWithoutExtension(nuspecEntry.Name)
82+
.Equals(Path.GetFileNameWithoutExtension(dll.Name), StringComparison.OrdinalIgnoreCase)) ?? dllEntry;
83+
}
84+
85+
dllStream = dllEntry.Open();
86+
var docEntry = zipArchive.GetEntry(Path.ChangeExtension(dllEntry.FullName, ".xml"));
87+
if (docEntry != null)
88+
{
89+
docStream = docEntry.Open();
90+
}
91+
using var nuspecStream = nuspecEntry.Open();
92+
var document = XDocument.Load(nuspecStream);
93+
var dependencyElements = document.Descendants().Where(e => e.Name.LocalName == "dependency");
94+
dependencies.AddRange(
95+
dependencyElements.Select(dependency => new DependencyInfo(
96+
dependency.Attribute("id")?.Value,
97+
SelectSpecificVersion(dependency.Attribute("version")?.Value))));
98+
// filter duplicates and sort
99+
if (dependencies.Any())
100+
{
101+
dependencies = dependencies
102+
.GroupBy(d => d.Name)
103+
.Select(d => d.First())
104+
.OrderBy(d => d.Name).ToList();
105+
}
67106
}
68107

69-
var dllEntry = dllEntries.First();
70-
if (dllEntries.Length > 1)
108+
IEnumerable<string> dependencyFilePaths = new List<string>();
109+
if (dependencies != null && dependencies.Any())
71110
{
72-
// If there are multiple dlls in the nupkg (e.g. Cosmos), try to find the first that matches the nuspec name, but
73-
// fallback to just using the first one.
74-
dllEntry = dllEntries.FirstOrDefault(
75-
dll => Path.GetFileNameWithoutExtension(nuspecEntry.Name)
76-
.Equals(Path.GetFileNameWithoutExtension(dll.Name), StringComparison.OrdinalIgnoreCase)) ?? dllEntry;
111+
dependencyFilesTempDir = await ExtractNugetDependencies(dependencies).ConfigureAwait(false);
112+
if (Directory.Exists(dependencyFilesTempDir))
113+
{
114+
dependencyFilePaths = Directory.EnumerateFiles(dependencyFilesTempDir, "*.dll", SearchOption.AllDirectories);
115+
}
77116
}
117+
var assemblySymbol = CompilationFactory.GetCompilation(dllStream, docStream, dependencyFilePaths);
78118

79-
dllStream = dllEntry.Open();
80-
var docEntry = zipArchive.GetEntry(Path.ChangeExtension(dllEntry.FullName, ".xml"));
81-
if (docEntry != null)
119+
if (assemblySymbol == null)
82120
{
83-
docStream = docEntry.Open();
121+
Console.Error.WriteLine($"PackageFile {packageFilePath.FullName} contains no Assembly Symbol.");
122+
return;
84123
}
85-
using var nuspecStream = nuspecEntry.Open();
86-
var document = XDocument.Load(nuspecStream);
87-
var dependencyElements = document.Descendants().Where(e => e.Name.LocalName == "dependency");
88-
dependencies.AddRange(
89-
dependencyElements.Select(dependency => new DependencyInfo(
90-
dependency.Attribute("id")?.Value,
91-
dependency.Attribute("version")?.Value)));
92-
// filter duplicates and sort
93-
if (dependencies.Any())
124+
125+
var parsedFileName = string.IsNullOrEmpty(outputFileName) ? assemblySymbol.Name : outputFileName;
126+
var treeTokenCodeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, runAnalysis, dependencies);
127+
var gzipJsonTokenFilePath = Path.Combine(OutputDirectory.FullName, $"{parsedFileName}.json.tgz");
128+
129+
130+
var options = new JsonSerializerOptions()
94131
{
95-
dependencies = dependencies
96-
.GroupBy(d => d.Name)
97-
.Select(d => d.First())
98-
.OrderBy(d => d.Name).ToList();
99-
}
132+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
133+
};
134+
135+
await using FileStream gzipFileStream = new FileStream(gzipJsonTokenFilePath, FileMode.Create, FileAccess.Write);
136+
await using GZipStream gZipStream = new GZipStream(gzipFileStream, CompressionLevel.Optimal);
137+
await JsonSerializer.SerializeAsync(gZipStream, treeTokenCodeFile, options);
138+
Console.WriteLine($"TokenCodeFile File {gzipJsonTokenFilePath} Generated Successfully.");
139+
Console.WriteLine();
100140
}
101-
102-
IEnumerable<string> dependencyFilePaths = new List<string>();
103-
if (dependencies != null && dependencies.Any())
141+
catch (Exception ex)
104142
{
105-
dependencyFilesTempDir = await ExtractNugetDependencies(dependencies).ConfigureAwait(false);
106-
dependencyFilePaths = Directory.EnumerateFiles(dependencyFilesTempDir, "*.dll", SearchOption.AllDirectories);
143+
Console.Error.WriteLine($"Error Parsing PackageFile : {ex.Message}");
107144
}
108-
var assemblySymbol = CompilationFactory.GetCompilation(dllStream, docStream, dependencyFilePaths);
109-
110-
if (assemblySymbol == null)
145+
finally
111146
{
112-
Console.Error.WriteLine($"PackageFile {packageFilePath.FullName} contains no Assembly Symbol.");
113-
return;
147+
zipArchive?.Dispose();
148+
if (dependencyFilesTempDir != null && Directory.Exists(dependencyFilesTempDir))
149+
{
150+
Directory.Delete(dependencyFilesTempDir, true);
151+
}
114152
}
115-
116-
var parsedFileName = string.IsNullOrEmpty(outputFileName) ? assemblySymbol.Name : outputFileName;
117-
var treeTokenCodeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, runAnalysis, dependencies);
118-
var gzipJsonTokenFilePath = Path.Combine(OutputDirectory.FullName, $"{parsedFileName}.json.tgz");
119-
120-
121-
var options = new JsonSerializerOptions()
122-
{
123-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
124-
};
125-
126-
await using FileStream gzipFileStream = new FileStream(gzipJsonTokenFilePath, FileMode.Create, FileAccess.Write);
127-
await using GZipStream gZipStream = new GZipStream(gzipFileStream, CompressionLevel.Optimal);
128-
await JsonSerializer.SerializeAsync(gZipStream, treeTokenCodeFile, options);
129-
Console.WriteLine($"TokenCodeFile File {gzipJsonTokenFilePath} Generated Successfully.");
130-
Console.WriteLine();
131153
}
132-
catch (Exception ex)
154+
155+
static bool IsNuget(string name)
133156
{
134-
Console.Error.WriteLine($"Error Parsing PackageFile : {ex.Message}");
157+
return name.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase);
135158
}
136-
finally
159+
160+
static bool IsNuspec(string name)
137161
{
138-
zipArchive?.Dispose();
139-
if (dependencyFilesTempDir != null && Directory.Exists(dependencyFilesTempDir))
140-
{
141-
Directory.Delete(dependencyFilesTempDir, true);
142-
}
162+
return name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase);
143163
}
144-
}
145164

146-
static bool IsNuget(string name)
147-
{
148-
return name.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase);
149-
}
150-
151-
static bool IsNuspec(string name)
152-
{
153-
return name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase);
154-
}
155-
156-
static bool IsDll(string name)
157-
{
158-
return name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase);
159-
}
165+
static bool IsDll(string name)
166+
{
167+
return name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase);
168+
}
160169

161-
/// <summary>
162-
/// Resolves the NuGet package dependencies and extracts them to a temporary folder. It is the responsibility of teh caller to clean up the folder.
163-
/// </summary>
164-
/// <param name="dependencyInfos">The dependency infos</param>
165-
/// <returns>A temporary path where the dependency files were extracted.</returns>
166-
static async Task<string> ExtractNugetDependencies(List<DependencyInfo> dependencyInfos)
167-
{
168-
string tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
169-
SourceCacheContext cache = new SourceCacheContext();
170-
SourceRepository repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json");
171-
try
170+
/// <summary>
171+
/// Resolves the NuGet package dependencies and extracts them to a temporary folder. It is the responsibility of teh caller to clean up the folder.
172+
/// </summary>
173+
/// <param name="dependencyInfos">The dependency infos</param>
174+
/// <returns>A temporary path where the dependency files were extracted.</returns>
175+
public static async Task<string> ExtractNugetDependencies(List<DependencyInfo> dependencyInfos)
172176
{
173-
FindPackageByIdResource resource = await repository.GetResourceAsync<FindPackageByIdResource>().ConfigureAwait(false);
174-
foreach (var dep in dependencyInfos)
177+
string tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
178+
SourceCacheContext cache = new SourceCacheContext();
179+
SourceRepository repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json");
180+
try
175181
{
176-
using (MemoryStream packageStream = new MemoryStream())
182+
FindPackageByIdResource resource = await repository.GetResourceAsync<FindPackageByIdResource>().ConfigureAwait(false);
183+
foreach (var dep in dependencyInfos)
177184
{
178-
if (await resource.CopyNupkgToStreamAsync(
179-
dep.Name,
180-
new NuGetVersion(dep.Version),
181-
packageStream,
182-
cache,
183-
NullLogger.Instance,
184-
CancellationToken.None))
185+
using (MemoryStream packageStream = new MemoryStream())
185186
{
186-
using PackageArchiveReader reader = new PackageArchiveReader(packageStream);
187-
NuspecReader nuspec = reader.NuspecReader;
188-
var file = reader.GetFiles().FirstOrDefault(f => f.EndsWith(dep.Name + ".dll"));
189-
if (file != null)
187+
if (await resource.CopyNupkgToStreamAsync(
188+
dep.Name,
189+
new NuGetVersion(dep.Version),
190+
packageStream,
191+
cache,
192+
NullLogger.Instance,
193+
CancellationToken.None))
190194
{
191-
var fileInfo = new FileInfo(file);
192-
var path = Path.Combine(tempFolder, dep.Name, fileInfo.Name);
193-
var tmp = reader.ExtractFile(file, path, NullLogger.Instance);
195+
using PackageArchiveReader reader = new PackageArchiveReader(packageStream);
196+
NuspecReader nuspec = reader.NuspecReader;
197+
var file = reader.GetFiles().FirstOrDefault(f => f.EndsWith(dep.Name + ".dll"));
198+
if (file != null)
199+
{
200+
var fileInfo = new FileInfo(file);
201+
var path = Path.Combine(tempFolder, dep.Name, fileInfo.Name);
202+
var tmp = reader.ExtractFile(file, path, NullLogger.Instance);
203+
}
194204
}
195205
}
196206
}
197207
}
208+
finally
209+
{
210+
cache.Dispose();
211+
}
212+
return tempFolder;
198213
}
199-
finally
214+
215+
public static string? SelectSpecificVersion(string? versionRange)
200216
{
201-
cache.Dispose();
217+
if (string.IsNullOrEmpty(versionRange))
218+
{
219+
return null;
220+
}
221+
var range = VersionRange.Parse(versionRange);
222+
if (range.HasUpperBound)
223+
{
224+
var maxVersion = range.MaxVersion;
225+
if (maxVersion != null)
226+
{
227+
if (range.IsMaxInclusive)
228+
{
229+
return maxVersion.ToString();
230+
}
231+
else
232+
{
233+
return maxVersion.Version.ToString();
234+
}
235+
}
236+
}
237+
var specificVersion = range.MinVersion;
238+
return specificVersion?.ToString();
202239
}
203-
return tempFolder;
204240
}

0 commit comments

Comments
 (0)