Skip to content

Commit c5f89ff

Browse files
authored
combine validation and errors into one result (#155)
1 parent 2582456 commit c5f89ff

File tree

205 files changed

+18596
-1723
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

205 files changed

+18596
-1723
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Usage: dotnet-project-licenses [options]
2525
**Options:**
2626

2727
| Option | Description |
28-
| --------------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
28+
|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
2929
| `-i, --input` | Project or Solution to be analyzed |
3030
| `-ji, --json-input` | Similar to `-i, --input` but providing a file containing a valid JSON Array that contains all projects to be analyzed |
3131
| `-t, --include-transitive` | When set, the analysis includes transitive packages (dependencies of packages that are directly installed to the project) |
@@ -35,6 +35,7 @@ Usage: dotnet-project-licenses [options]
3535
| `-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. |
3636
| `-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. |
3737
| `-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. |
38+
| `-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. |
3839

3940
## Example tool commands
4041

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace NuGetUtility.Extension
2+
{
3+
public static class AsyncEnumerableExtension
4+
{
5+
public static async IAsyncEnumerable<TResult> SelectMany<TSource, TResult>(this IAsyncEnumerable<TSource> input,
6+
Func<TSource, IAsyncEnumerable<TResult>> transform)
7+
{
8+
await foreach (var value in input)
9+
{
10+
await foreach (var transformedValue in transform(value))
11+
{
12+
yield return transformedValue;
13+
}
14+
}
15+
}
16+
public static async IAsyncEnumerable<TResult> SelectMany<TSource, TResult>(this IEnumerable<TSource> input,
17+
Func<TSource, IAsyncEnumerable<TResult>> transform)
18+
{
19+
foreach (var value in input)
20+
{
21+
await foreach (var transformedValue in transform(value))
22+
{
23+
yield return transformedValue;
24+
}
25+
}
26+
}
27+
}
28+
}

src/NuGetUtility/LicenseValidator/LicenseInformationOrigin.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
public enum LicenseInformationOrigin
44
{
55
Expression,
6-
Url
6+
Url,
7+
Unknown
78
}
89
}

src/NuGetUtility/LicenseValidator/LicenseValidationError.cs

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using NuGet.Versioning;
2+
3+
namespace NuGetUtility.LicenseValidator
4+
{
5+
public record LicenseValidationResult(string PackageId,
6+
NuGetVersion PackageVersion,
7+
string? PackageProjectUrl,
8+
string? License,
9+
LicenseInformationOrigin LicenseInformationOrigin,
10+
List<ValidationError>? ValidationErrors = null)
11+
{
12+
public List<ValidationError> ValidationErrors { get; } = ValidationErrors ?? new List<ValidationError>();
13+
14+
public string? License { get; set; } = License;
15+
public LicenseInformationOrigin LicenseInformationOrigin { get; set; } = LicenseInformationOrigin;
16+
}
17+
}
Lines changed: 105 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
using NuGet.Packaging;
22
using NuGet.Protocol.Core.Types;
3+
using NuGet.Versioning;
4+
using NuGetUtility.PackageInformationReader;
35
using NuGetUtility.Wrapper.HttpClientWrapper;
6+
using System.Collections.Concurrent;
47

58
namespace NuGetUtility.LicenseValidator
69
{
710
public class LicenseValidator
811
{
912
private readonly IEnumerable<string> _allowedLicenses;
10-
private readonly List<LicenseValidationError> _errors = new List<LicenseValidationError>();
1113
private readonly IFileDownloader _fileDownloader;
1214
private readonly Dictionary<Uri, string> _licenseMapping;
13-
private readonly HashSet<ValidatedLicense> _validatedLicenses = new HashSet<ValidatedLicense>();
1415

1516
public LicenseValidator(Dictionary<Uri, string> licenseMapping,
1617
IEnumerable<string> allowedLicenses,
@@ -21,70 +22,124 @@ public LicenseValidator(Dictionary<Uri, string> licenseMapping,
2122
_fileDownloader = fileDownloader;
2223
}
2324

24-
public async Task Validate(IAsyncEnumerable<IPackageSearchMetadata> downloadedInfo, string context)
25+
public async Task<IEnumerable<LicenseValidationResult>> Validate(
26+
IAsyncEnumerable<ReferencedPackageWithContext> packages)
2527
{
26-
await foreach (var info in downloadedInfo)
28+
var result = new ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult>();
29+
await foreach (var info in packages)
2730
{
28-
if (info.LicenseMetadata != null)
31+
if (info.PackageInfo.LicenseMetadata != null)
2932
{
30-
ValidateLicenseByMetadata(info, context);
33+
ValidateLicenseByMetadata(info.PackageInfo, info.Context, result);
3134
}
32-
else if (info.LicenseUrl != null)
35+
else if (info.PackageInfo.LicenseUrl != null)
3336
{
34-
await ValidateLicenseByUrl(info, context);
37+
await ValidateLicenseByUrl(info.PackageInfo, info.Context, result);
3538
}
3639
else
3740
{
38-
_errors.Add(new LicenseValidationError(context,
39-
info.Identity.Id,
40-
info.Identity.Version,
41-
"No license information found"));
41+
AddOrUpdateLicense(result,
42+
info.PackageInfo,
43+
LicenseInformationOrigin.Unknown,
44+
new ValidationError("No license information found", info.Context));
4245
}
4346
}
47+
return result.Values;
4448
}
4549

46-
public IEnumerable<LicenseValidationError> GetErrors()
50+
private void AddOrUpdateLicense(
51+
ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult> result,
52+
IPackageSearchMetadata info,
53+
LicenseInformationOrigin origin,
54+
ValidationError error,
55+
string? license = null)
4756
{
48-
return _errors;
57+
var newValue = new LicenseValidationResult(
58+
info.Identity.Id,
59+
info.Identity.Version,
60+
info.ProjectUrl?.ToString(),
61+
license,
62+
origin,
63+
new List<ValidationError> { error });
64+
result.AddOrUpdate(new LicenseNameAndVersion(info.Identity.Id, info.Identity.Version),
65+
key => CreateResult(key, newValue),
66+
(key, oldValue) => UpdateResult(key, oldValue, newValue));
4967
}
5068

51-
public IEnumerable<ValidatedLicense> GetValidatedLicenses()
69+
private void AddOrUpdateLicense(
70+
ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult> result,
71+
IPackageSearchMetadata info,
72+
LicenseInformationOrigin origin,
73+
string? license = null)
5274
{
53-
return _validatedLicenses;
75+
var newValue = new LicenseValidationResult(
76+
info.Identity.Id,
77+
info.Identity.Version,
78+
info.ProjectUrl?.ToString(),
79+
license,
80+
origin);
81+
result.AddOrUpdate(new LicenseNameAndVersion(info.Identity.Id, info.Identity.Version),
82+
key => CreateResult(key, newValue),
83+
(key, oldValue) => UpdateResult(key, oldValue, newValue));
5484
}
5585

56-
private void ValidateLicenseByMetadata(IPackageSearchMetadata info, string context)
86+
private LicenseValidationResult UpdateResult(LicenseNameAndVersion _,
87+
LicenseValidationResult oldValue,
88+
LicenseValidationResult newValue)
89+
{
90+
oldValue.ValidationErrors.AddRange(newValue.ValidationErrors);
91+
if (oldValue.License is null && newValue.License is not null)
92+
{
93+
oldValue.License = newValue.License;
94+
oldValue.LicenseInformationOrigin = newValue.LicenseInformationOrigin;
95+
}
96+
return oldValue;
97+
}
98+
99+
private LicenseValidationResult CreateResult(LicenseNameAndVersion _, LicenseValidationResult newValue)
100+
{
101+
return newValue;
102+
}
103+
104+
private void ValidateLicenseByMetadata(IPackageSearchMetadata info,
105+
string context,
106+
ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult> result)
57107
{
58108
switch (info.LicenseMetadata!.Type)
59109
{
60110
case LicenseType.Expression:
61111
var licenseId = info.LicenseMetadata!.License;
62112
if (IsLicenseValid(licenseId))
63113
{
64-
_validatedLicenses.Add(new ValidatedLicense(info.Identity.Id,
65-
info.Identity.Version,
66-
info.LicenseMetadata.License,
67-
LicenseInformationOrigin.Expression));
114+
AddOrUpdateLicense(result,
115+
info,
116+
LicenseInformationOrigin.Expression,
117+
info.LicenseMetadata.License);
68118
}
69119
else
70120
{
71-
_errors.Add(new LicenseValidationError(context,
72-
info.Identity.Id,
73-
info.Identity.Version,
74-
GetLicenseNotAllowedMessage(info.LicenseMetadata.License)));
121+
AddOrUpdateLicense(result,
122+
info,
123+
LicenseInformationOrigin.Expression,
124+
new ValidationError(GetLicenseNotAllowedMessage(info.LicenseMetadata.License), context),
125+
info.LicenseMetadata.License);
75126
}
76127

77128
break;
78129
default:
79-
_errors.Add(new LicenseValidationError(context,
80-
info.Identity.Id,
81-
info.Identity.Version,
82-
$"Validation for licenses of type {info.LicenseMetadata!.Type} not yet supported"));
130+
AddOrUpdateLicense(result,
131+
info,
132+
LicenseInformationOrigin.Unknown,
133+
new ValidationError(
134+
$"Validation for licenses of type {info.LicenseMetadata!.Type} not yet supported",
135+
context));
83136
break;
84137
}
85138
}
86139

87-
private async Task ValidateLicenseByUrl(IPackageSearchMetadata info, string context)
140+
private async Task ValidateLicenseByUrl(IPackageSearchMetadata info,
141+
string context,
142+
ConcurrentDictionary<LicenseNameAndVersion, LicenseValidationResult> result)
88143
{
89144
if (info.LicenseUrl.IsAbsoluteUri)
90145
{
@@ -103,32 +158,34 @@ await _fileDownloader.DownloadFile(info.LicenseUrl,
103158
{
104159
if (IsLicenseValid(licenseId))
105160
{
106-
_validatedLicenses.Add(new ValidatedLicense(info.Identity.Id,
107-
info.Identity.Version,
108-
licenseId,
109-
LicenseInformationOrigin.Url));
161+
AddOrUpdateLicense(result,
162+
info,
163+
LicenseInformationOrigin.Url,
164+
licenseId);
110165
}
111166
else
112167
{
113-
_errors.Add(new LicenseValidationError(context,
114-
info.Identity.Id,
115-
info.Identity.Version,
116-
GetLicenseNotAllowedMessage(licenseId)));
168+
AddOrUpdateLicense(result,
169+
info,
170+
LicenseInformationOrigin.Url,
171+
new ValidationError(GetLicenseNotAllowedMessage(licenseId), context),
172+
licenseId);
117173
}
118174
}
119175
else if (!_allowedLicenses.Any())
120176
{
121-
_validatedLicenses.Add(new ValidatedLicense(info.Identity.Id,
122-
info.Identity.Version,
123-
info.LicenseUrl.ToString(),
124-
LicenseInformationOrigin.Url));
177+
AddOrUpdateLicense(result,
178+
info,
179+
LicenseInformationOrigin.Url,
180+
info.LicenseUrl.ToString());
125181
}
126182
else
127183
{
128-
_errors.Add(new LicenseValidationError(context,
129-
info.Identity.Id,
130-
info.Identity.Version,
131-
$"Cannot determine License type for url {info.LicenseUrl}"));
184+
AddOrUpdateLicense(result,
185+
info,
186+
LicenseInformationOrigin.Url,
187+
new ValidationError($"Cannot determine License type for url {info.LicenseUrl}", context),
188+
info.LicenseUrl.ToString());
132189
}
133190
}
134191

@@ -154,5 +211,7 @@ private string GetLicenseNotAllowedMessage(string license)
154211
{
155212
return $"License {license} not found in list of supported licenses";
156213
}
214+
215+
private record LicenseNameAndVersion(string LicenseName, NuGetVersion Version);
157216
}
158217
}

src/NuGetUtility/LicenseValidator/ValidatedLicense.cs

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace NuGetUtility.LicenseValidator
2+
{
3+
public record ValidationError(string Error, string Context);
4+
}

src/NuGetUtility/Output/IOuputFormatter.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using NuGetUtility.LicenseValidator;
2+
3+
namespace NuGetUtility.Output
4+
{
5+
public interface IOutputFormatter
6+
{
7+
Task Write(Stream stream, IList<LicenseValidationResult> results);
8+
}
9+
}

src/NuGetUtility/Output/Json/JsonOutputFormatter.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,32 @@ namespace NuGetUtility.Output.Json
55
{
66
public class JsonOutputFormatter : IOutputFormatter
77
{
8+
private readonly bool _printErrorsOnly;
89
private readonly JsonSerializerOptions _options;
9-
public JsonOutputFormatter(bool prettyPrint = false)
10+
public JsonOutputFormatter(bool prettyPrint = false, bool printErrorsOnly = false)
1011
{
12+
_printErrorsOnly = printErrorsOnly;
1113
_options = new JsonSerializerOptions
1214
{
13-
Converters = { new NuGetVersionJsonConverter() },
15+
Converters =
16+
{ new NuGetVersionJsonConverter(), new ValidatedLicenseJsonConverterWithOmittingEmptyErrorList() },
1417
WriteIndented = prettyPrint
1518
};
1619
}
1720

18-
public async Task Write(Stream stream, IEnumerable<LicenseValidationError> errors)
21+
public async Task Write(Stream stream, IList<LicenseValidationResult> results)
1922
{
20-
await JsonSerializer.SerializeAsync(stream, errors, _options);
21-
}
22-
public async Task Write(Stream stream, IEnumerable<ValidatedLicense> validated)
23-
{
24-
await JsonSerializer.SerializeAsync(stream, validated, _options);
23+
if (_printErrorsOnly)
24+
{
25+
var resultsWithErrors = results.Where(r => r.ValidationErrors.Any()).ToList();
26+
if (resultsWithErrors.Any())
27+
{
28+
await JsonSerializer.SerializeAsync(stream, resultsWithErrors, _options);
29+
return;
30+
}
31+
}
32+
33+
await JsonSerializer.SerializeAsync(stream, results, _options);
2534
}
2635
}
2736
}

0 commit comments

Comments
 (0)