Skip to content

Commit

Permalink
combine validation and errors into one result (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
sensslen authored Nov 1, 2022
1 parent 2582456 commit c5f89ff
Show file tree
Hide file tree
Showing 205 changed files with 18,596 additions and 1,723 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Usage: dotnet-project-licenses [options]
**Options:**

| Option | Description |
| --------------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-i, --input` | Project or Solution to be analyzed |
| `-ji, --json-input` | Similar to `-i, --input` but providing a file containing a valid JSON Array that contains all projects to be analyzed |
| `-t, --include-transitive` | When set, the analysis includes transitive packages (dependencies of packages that are directly installed to the project) |
Expand All @@ -35,6 +35,7 @@ Usage: dotnet-project-licenses [options]
| `-override, --override-package-information` | When used, this option allows to override the package information used for the validation. This makes sure that no attempt is made to get the associated information about the package from the available web resources. This is useful for packages that e.g. provide a license file as part of the nuget package which (at the time of writing) cannot be used for validation and thus requires the package's information to be provided by this option. |
| `-d, --license-information-download-location` | When used, this option downloads the html content of the license URL to the specified folder. This is done for all NuGet packages that specify a license URL instead of providing the license expression. |
| `-o, --output` | This Parameter accepts the value `table`, `json` or `jsonPretty`. It allows to select the type of output that should be given. If omitted, the output is given in tabular form. |
| `-err, --error-only` | This flag allows to print only packages that contain validation errors (if there are any). This allows the user to focus on errors instead of having to deal with many properly validated licenses. |

## Example tool commands

Expand Down
28 changes: 28 additions & 0 deletions src/NuGetUtility/Extension/AsyncEnumerableExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace NuGetUtility.Extension
{
public static class AsyncEnumerableExtension
{
public static async IAsyncEnumerable<TResult> SelectMany<TSource, TResult>(this IAsyncEnumerable<TSource> input,
Func<TSource, IAsyncEnumerable<TResult>> transform)
{
await foreach (var value in input)
{
await foreach (var transformedValue in transform(value))
{
yield return transformedValue;
}
}
}
public static async IAsyncEnumerable<TResult> SelectMany<TSource, TResult>(this IEnumerable<TSource> input,
Func<TSource, IAsyncEnumerable<TResult>> transform)
{
foreach (var value in input)
{
await foreach (var transformedValue in transform(value))
{
yield return transformedValue;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public enum LicenseInformationOrigin
{
Expression,
Url
Url,
Unknown
}
}
9 changes: 0 additions & 9 deletions src/NuGetUtility/LicenseValidator/LicenseValidationError.cs

This file was deleted.

17 changes: 17 additions & 0 deletions src/NuGetUtility/LicenseValidator/LicenseValidationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using NuGet.Versioning;

namespace NuGetUtility.LicenseValidator
{
public record LicenseValidationResult(string PackageId,
NuGetVersion PackageVersion,
string? PackageProjectUrl,
string? License,
LicenseInformationOrigin LicenseInformationOrigin,
List<ValidationError>? ValidationErrors = null)
{
public List<ValidationError> ValidationErrors { get; } = ValidationErrors ?? new List<ValidationError>();

public string? License { get; set; } = License;
public LicenseInformationOrigin LicenseInformationOrigin { get; set; } = LicenseInformationOrigin;
}
}
151 changes: 105 additions & 46 deletions src/NuGetUtility/LicenseValidator/LicenseValidator.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using NuGet.Packaging;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using NuGetUtility.PackageInformationReader;
using NuGetUtility.Wrapper.HttpClientWrapper;
using System.Collections.Concurrent;

namespace NuGetUtility.LicenseValidator
{
public class LicenseValidator
{
private readonly IEnumerable<string> _allowedLicenses;
private readonly List<LicenseValidationError> _errors = new List<LicenseValidationError>();
private readonly IFileDownloader _fileDownloader;
private readonly Dictionary<Uri, string> _licenseMapping;
private readonly HashSet<ValidatedLicense> _validatedLicenses = new HashSet<ValidatedLicense>();

public LicenseValidator(Dictionary<Uri, string> licenseMapping,
IEnumerable<string> allowedLicenses,
Expand All @@ -21,70 +22,124 @@ public LicenseValidator(Dictionary<Uri, string> licenseMapping,
_fileDownloader = fileDownloader;
}

public async Task Validate(IAsyncEnumerable<IPackageSearchMetadata> downloadedInfo, string context)
public async Task<IEnumerable<LicenseValidationResult>> Validate(
IAsyncEnumerable<ReferencedPackageWithContext> packages)
{
await foreach (var info in downloadedInfo)
var result = new ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult>();
await foreach (var info in packages)
{
if (info.LicenseMetadata != null)
if (info.PackageInfo.LicenseMetadata != null)
{
ValidateLicenseByMetadata(info, context);
ValidateLicenseByMetadata(info.PackageInfo, info.Context, result);
}
else if (info.LicenseUrl != null)
else if (info.PackageInfo.LicenseUrl != null)
{
await ValidateLicenseByUrl(info, context);
await ValidateLicenseByUrl(info.PackageInfo, info.Context, result);
}
else
{
_errors.Add(new LicenseValidationError(context,
info.Identity.Id,
info.Identity.Version,
"No license information found"));
AddOrUpdateLicense(result,
info.PackageInfo,
LicenseInformationOrigin.Unknown,
new ValidationError("No license information found", info.Context));
}
}
return result.Values;
}

public IEnumerable<LicenseValidationError> GetErrors()
private void AddOrUpdateLicense(
ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult> result,
IPackageSearchMetadata info,
LicenseInformationOrigin origin,
ValidationError error,
string? license = null)
{
return _errors;
var newValue = new LicenseValidationResult(
info.Identity.Id,
info.Identity.Version,
info.ProjectUrl?.ToString(),
license,
origin,
new List<ValidationError> { error });
result.AddOrUpdate(new LicenseNameAndVersion(info.Identity.Id, info.Identity.Version),
key => CreateResult(key, newValue),
(key, oldValue) => UpdateResult(key, oldValue, newValue));
}

public IEnumerable<ValidatedLicense> GetValidatedLicenses()
private void AddOrUpdateLicense(
ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult> result,
IPackageSearchMetadata info,
LicenseInformationOrigin origin,
string? license = null)
{
return _validatedLicenses;
var newValue = new LicenseValidationResult(
info.Identity.Id,
info.Identity.Version,
info.ProjectUrl?.ToString(),
license,
origin);
result.AddOrUpdate(new LicenseNameAndVersion(info.Identity.Id, info.Identity.Version),
key => CreateResult(key, newValue),
(key, oldValue) => UpdateResult(key, oldValue, newValue));
}

private void ValidateLicenseByMetadata(IPackageSearchMetadata info, string context)
private LicenseValidationResult UpdateResult(LicenseNameAndVersion _,
LicenseValidationResult oldValue,
LicenseValidationResult newValue)
{
oldValue.ValidationErrors.AddRange(newValue.ValidationErrors);
if (oldValue.License is null && newValue.License is not null)
{
oldValue.License = newValue.License;
oldValue.LicenseInformationOrigin = newValue.LicenseInformationOrigin;
}
return oldValue;
}

private LicenseValidationResult CreateResult(LicenseNameAndVersion _, LicenseValidationResult newValue)
{
return newValue;
}

private void ValidateLicenseByMetadata(IPackageSearchMetadata info,
string context,
ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult> result)
{
switch (info.LicenseMetadata!.Type)
{
case LicenseType.Expression:
var licenseId = info.LicenseMetadata!.License;
if (IsLicenseValid(licenseId))
{
_validatedLicenses.Add(new ValidatedLicense(info.Identity.Id,
info.Identity.Version,
info.LicenseMetadata.License,
LicenseInformationOrigin.Expression));
AddOrUpdateLicense(result,
info,
LicenseInformationOrigin.Expression,
info.LicenseMetadata.License);
}
else
{
_errors.Add(new LicenseValidationError(context,
info.Identity.Id,
info.Identity.Version,
GetLicenseNotAllowedMessage(info.LicenseMetadata.License)));
AddOrUpdateLicense(result,
info,
LicenseInformationOrigin.Expression,
new ValidationError(GetLicenseNotAllowedMessage(info.LicenseMetadata.License), context),
info.LicenseMetadata.License);
}

break;
default:
_errors.Add(new LicenseValidationError(context,
info.Identity.Id,
info.Identity.Version,
$"Validation for licenses of type {info.LicenseMetadata!.Type} not yet supported"));
AddOrUpdateLicense(result,
info,
LicenseInformationOrigin.Unknown,
new ValidationError(
$"Validation for licenses of type {info.LicenseMetadata!.Type} not yet supported",
context));
break;
}
}

private async Task ValidateLicenseByUrl(IPackageSearchMetadata info, string context)
private async Task ValidateLicenseByUrl(IPackageSearchMetadata info,
string context,
ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult> result)
{
if (info.LicenseUrl.IsAbsoluteUri)
{
Expand All @@ -103,32 +158,34 @@ await _fileDownloader.DownloadFile(info.LicenseUrl,
{
if (IsLicenseValid(licenseId))
{
_validatedLicenses.Add(new ValidatedLicense(info.Identity.Id,
info.Identity.Version,
licenseId,
LicenseInformationOrigin.Url));
AddOrUpdateLicense(result,
info,
LicenseInformationOrigin.Url,
licenseId);
}
else
{
_errors.Add(new LicenseValidationError(context,
info.Identity.Id,
info.Identity.Version,
GetLicenseNotAllowedMessage(licenseId)));
AddOrUpdateLicense(result,
info,
LicenseInformationOrigin.Url,
new ValidationError(GetLicenseNotAllowedMessage(licenseId), context),
licenseId);
}
}
else if (!_allowedLicenses.Any())
{
_validatedLicenses.Add(new ValidatedLicense(info.Identity.Id,
info.Identity.Version,
info.LicenseUrl.ToString(),
LicenseInformationOrigin.Url));
AddOrUpdateLicense(result,
info,
LicenseInformationOrigin.Url,
info.LicenseUrl.ToString());
}
else
{
_errors.Add(new LicenseValidationError(context,
info.Identity.Id,
info.Identity.Version,
$"Cannot determine License type for url {info.LicenseUrl}"));
AddOrUpdateLicense(result,
info,
LicenseInformationOrigin.Url,
new ValidationError($"Cannot determine License type for url {info.LicenseUrl}", context),
info.LicenseUrl.ToString());
}
}

Expand All @@ -154,5 +211,7 @@ private string GetLicenseNotAllowedMessage(string license)
{
return $"License {license} not found in list of supported licenses";
}

private record LicenseNameAndVersion(string LicenseName, NuGetVersion Version);
}
}
9 changes: 0 additions & 9 deletions src/NuGetUtility/LicenseValidator/ValidatedLicense.cs

This file was deleted.

4 changes: 4 additions & 0 deletions src/NuGetUtility/LicenseValidator/ValidationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace NuGetUtility.LicenseValidator
{
public record ValidationError(string Error, string Context);
}
10 changes: 0 additions & 10 deletions src/NuGetUtility/Output/IOuputFormatter.cs

This file was deleted.

9 changes: 9 additions & 0 deletions src/NuGetUtility/Output/IOutputFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using NuGetUtility.LicenseValidator;

namespace NuGetUtility.Output
{
public interface IOutputFormatter
{
Task Write(Stream stream, IList<LicenseValidationResult> results);
}
}
25 changes: 17 additions & 8 deletions src/NuGetUtility/Output/Json/JsonOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,32 @@ namespace NuGetUtility.Output.Json
{
public class JsonOutputFormatter : IOutputFormatter
{
private readonly bool _printErrorsOnly;
private readonly JsonSerializerOptions _options;
public JsonOutputFormatter(bool prettyPrint = false)
public JsonOutputFormatter(bool prettyPrint = false, bool printErrorsOnly = false)
{
_printErrorsOnly = printErrorsOnly;
_options = new JsonSerializerOptions
{
Converters = { new NuGetVersionJsonConverter() },
Converters =
{ new NuGetVersionJsonConverter(), new ValidatedLicenseJsonConverterWithOmittingEmptyErrorList() },
WriteIndented = prettyPrint
};
}

public async Task Write(Stream stream, IEnumerable<LicenseValidationError> errors)
public async Task Write(Stream stream, IList<LicenseValidationResult> results)
{
await JsonSerializer.SerializeAsync(stream, errors, _options);
}
public async Task Write(Stream stream, IEnumerable<ValidatedLicense> validated)
{
await JsonSerializer.SerializeAsync(stream, validated, _options);
if (_printErrorsOnly)
{
var resultsWithErrors = results.Where(r => r.ValidationErrors.Any()).ToList();
if (resultsWithErrors.Any())
{
await JsonSerializer.SerializeAsync(stream, resultsWithErrors, _options);
return;
}
}

await JsonSerializer.SerializeAsync(stream, results, _options);
}
}
}
Loading

0 comments on commit c5f89ff

Please sign in to comment.