Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The NuGet branch of commands have recursively from given path inventories packag
* packages.config
* .NET Tool manifests
* Cake build script files
* C# file-based apps (`*.cs` files that start with `#!/usr/bin/env dotnet`, inventorying `#:sdk` and `#:package`; omitted `#:sdk` versions are taken from `msbuild-sdks` in `global.json`)

#### analyze

Expand Down
3 changes: 3 additions & 0 deletions resources/src/FileBasedApp/app.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env dotnet
#:sdk Cake.Sdk
#:package Spectre.Console
8 changes: 8 additions & 0 deletions resources/src/FileBasedApp/global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"sdk": {
"version": "10.0.401"
},
"msbuild-sdks": {
"Cake.Sdk": "6.3.0"
}
}
8 changes: 7 additions & 1 deletion src/DPI/Commands/NuGet/NuGetAnalyzeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace DPI.Commands.NuGet
// ReSharper disable once ClassNeverInstantiated.Global
public class NuGetAnalyzeCommand<TSettings> : NuGetCommand<TSettings> where TSettings : NuGetAnalyzeSettings
{
private const string NuGetPackagesFilePattern = "{.csproj,dotnet-tools.json,packages.config,.cake}";
private const string NuGetPackagesFilePattern = "{.csproj,dotnet-tools.json,packages.config,.cake,.cs}";

private NuGetParsers NuGetParsers { get; }

Expand Down Expand Up @@ -141,6 +141,12 @@ NuGetAnalyzeSettings settings
gitFolder,
filePackageReference
),
(_, ".cs") => NuGetParsers.FileBasedAppParser.Parse(
settings,
filePath,
gitFolder,
filePackageReference
),
_ => AsyncEnumerable.Empty<PackageReference>()
})
{
Expand Down
5 changes: 5 additions & 0 deletions src/DPI/Models/NuGet/GlobalJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace DPI.Models.NuGet;

public record GlobalJson(
[property: JsonPropertyName("msbuild-sdks")] Dictionary<string, string>? MsBuildSdks
);
6 changes: 6 additions & 0 deletions src/DPI/Models/NuGet/GlobalJsonSerializerContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;

namespace DPI.Models.NuGet;

[JsonSerializable(typeof(GlobalJson))]
internal partial class GlobalJsonSerializerContext : JsonSerializerContext;
3 changes: 2 additions & 1 deletion src/DPI/Models/NuGet/NuGetSourceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public enum NuGetSourceType
PackagesConfig,
DotNetToolsManifest,
ProjectAssets,
Cake
Cake,
FileBasedApp
}
}
127 changes: 127 additions & 0 deletions src/DPI/Parsers/NuGet/FileBasedAppParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Text.RegularExpressions;
using Cake.Common.IO;
using DPI.Commands.Settings.NuGet;
using DPI.Models.NuGet;

namespace DPI.Parsers.NuGet;

public partial class FileBasedAppParser : INuGetPackageReferenceParser
{
public NuGetSourceType SourceType { get; } = NuGetSourceType.FileBasedApp;

[GeneratedRegex(@"^#!/usr/bin/env dotnet(?:\s|$)", RegexOptions.CultureInvariant)]
private static partial Regex Shebang();

[GeneratedRegex(@"^#:(sdk|package)\s+([^@]+?)(?:@(.+))?$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex PackageOrSdkDirective();

public async IAsyncEnumerable<PackageReference> Parse(
NuGetSettings settings,
FilePath filePath,
DirectoryPath? repoRootPath,
PackageReference basePackageReference
)
{
using (settings.Logger.BeginScope(nameof(FileBasedAppParser)))
{
await using var file = settings.Context.FileSystem.GetFile(filePath).OpenRead();
using var reader = new StreamReader(file, Encoding.UTF8);

var firstLine = await reader.ReadLineAsync();
if (firstLine is null || !Shebang().IsMatch(firstLine.TrimStart()))
{
yield break;
}

ILookup<string, string>? msBuildSdks = null;
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
var trimmed = line.Trim();

if (trimmed.Length == 0 || trimmed.StartsWith("//", StringComparison.Ordinal))
{
continue;
}

if (!trimmed.StartsWith("#:", StringComparison.Ordinal))
{
yield break;
}

var match = PackageOrSdkDirective().Match(trimmed);
if (!match.Success)
{
continue;
}

var kind = match.Groups[1].Value;
var packageId = match.Groups[2].Value.Trim();
var version = match.Groups[3].Success
? match.Groups[3].Value.Trim()
: null;

if (string.IsNullOrWhiteSpace(packageId))
{
continue;
}

if (string.IsNullOrWhiteSpace(version)
&& kind.Equals("sdk", StringComparison.OrdinalIgnoreCase))
{
msBuildSdks ??= await TryFindMsBuildSdks(settings, filePath, repoRootPath);
version = msBuildSdks[packageId].FirstOrDefault();
}

yield return basePackageReference with
{
SourceType = SourceType,
PackageId = packageId,
Version = string.IsNullOrWhiteSpace(version) ? null : version
};
}
}
}

private static async Task<ILookup<string, string>> TryFindMsBuildSdks(
NuGetSettings settings,
FilePath filePath,
DirectoryPath? gitRootPath
)
{
var currentDirectory = settings.Context.MakeAbsolute(filePath.GetDirectory()).Collapse();
var stopDir = gitRootPath is { }
? settings.Context.MakeAbsolute(gitRootPath.Combine("../")).Collapse()
: null;

for (
var level = byte.MaxValue
; level > 0
&& currentDirectory.FullPath != string.Empty
&& currentDirectory.FullPath != stopDir?.FullPath
; level--
)
{
var globalJsonPath = currentDirectory.CombineWithFilePath("global.json");

if (settings.Context.FileExists(globalJsonPath))
{
await using var globalJsonFile = settings.Context.FileSystem.GetFile(globalJsonPath).OpenRead();
var globalJson = await JsonSerializer.DeserializeAsync(
globalJsonFile,
GlobalJsonSerializerContext.Default.GlobalJson);

return (globalJson?.MsBuildSdks ?? new Dictionary<string, string>())
.ToLookup(
pair => pair.Key,
pair => pair.Value,
StringComparer.OrdinalIgnoreCase
);
}

currentDirectory = currentDirectory.Combine("../").Collapse();
}

return Array.Empty<(string key, string value)>().ToLookup(pair => pair.key, pair => pair.value);
}
}
5 changes: 4 additions & 1 deletion src/DPI/Parsers/NuGet/NuGetParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ public class NuGetParsers
public INuGetPackageReferenceParser DotNetToolsManifestParser { get; }
public INuGetPackageReferenceParser PackageConfigParser { get; }
public INuGetPackageReferenceParser CakeParser { get; }
public INuGetPackageReferenceParser FileBasedAppParser { get; }

public NuGetParsers(
CsProjParser csProjParser,
DotNetToolsManifestParser dotNetToolsManifestParser,
PackageConfigParser packageConfigParser,
CakeParser cakeParser
CakeParser cakeParser,
FileBasedAppParser fileBasedAppParser
)
{
CsProjParser = csProjParser;
DotNetToolsManifestParser = dotNetToolsManifestParser;
PackageConfigParser = packageConfigParser;
CakeParser = cakeParser;
FileBasedAppParser = fileBasedAppParser;
}
}
1 change: 1 addition & 0 deletions src/DPI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ static partial void AddServices(IServiceCollection services)
.AddSingleton<DotNetToolsManifestParser>()
.AddSingleton<PackageConfigParser>()
.AddSingleton<CakeParser>()
.AddSingleton<FileBasedAppParser>()
.AddSingleton<NuGetParsers>()
.AddSingleton<ProjectsAssetsParser>()
.AddSingleton<IOutputConverter, JsonOutputConverter>()
Expand Down
Loading