From 01867e0d0a22a5428e234f559e1254f2e996c10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Wagenf=C3=BChr?= Date: Sun, 3 Sep 2023 00:00:34 +0200 Subject: [PATCH] Release/2.1 (#151) * feat: enable export to json file (#142) * feat: enable export to json file Closes #16 * docs: add documentation * docs(readme): update readme * chore(deps)!: upgrade SlnParser Closes #148 * chore: enable trimmable Closes #144 * feature: add the ability to filter projects using glob-patterns (#149) * feat: implement project exclusion Closes #5 * refactor(tool): rebuild Liz.Tool as this no longer works like we want as we exceeeded the maximum amount of parameters * docs: update documentation * Feature/7 exclude packages (#150) * feat: add the ability to filter packages using glob-patterns Closes #7 * log: add log-message to make debugging easier --- README.md | 6 +- doc/documenation-cake-addin.md | 37 ++ doc/documentation-dotnet-tool.md | 63 +++ doc/documentation-nuke-addon.md | 32 ++ src/Cake/Liz.Cake/Liz.Cake.csproj | 1 + .../GetProjectReferencesTests.cs | 2 +- src/Core/Liz.Core.Tests/Liz.Core.Tests.csproj | 14 +- .../GetPackageReferencesFacadeTests.cs | 46 +++ ...ePackageExclusionGlobsPreprocessorTests.cs | 157 +++++++ ...eProjectExclusionGlobsPreprocessorTests.cs | 157 +++++++ .../Projects/GetProjectsViaSlnParserTests.cs | 79 ++++ .../ExportToJsonResultProcessorTests.cs | 77 ++++ src/Core/Liz.Core/ExtractLicensesFactory.cs | 10 +- ...hPackageReferenceWithLicenseInformation.cs | 1 + src/Core/Liz.Core/Liz.Core.csproj | 4 +- .../GetPackageReferencesFacade.cs | 41 +- ...ializePackageExclusionGlobsPreprocessor.cs | 60 +++ ...ializeProjectExclusionGlobsPreprocessor.cs | 60 +++ .../Projects/GetProjectsViaSlnParser.cs | 29 +- .../ExportLicenseTextsResultProcessor.cs | 27 +- .../Result/ExportToJsonResultProcessor.cs | 65 +++ .../Settings/ExtractLicensesSettingsBase.cs | 84 ++++ src/Liz.sln | 15 - .../ExtractLicensesSettingsExtensions.cs | 385 +++++++++++++++--- src/Nuke/Liz.Nuke/Liz.Nuke.csproj | 1 + .../CommandLine/CommandProviderTests.cs | 306 -------------- .../CommandLine/CommandRunnerTests.cs | 83 ---- src/Tool/Liz.Tool.Tests/Liz.Tool.Tests.csproj | 28 -- .../Liz.Tool/CommandLine/CommandProvider.cs | 196 --------- .../Liz.Tool/CommandLine/CommandRunner.cs | 97 ----- .../Contracts/CommandLine/ICommandRunner.cs | 20 - src/Tool/Liz.Tool/Liz.Tool.csproj | 3 +- src/Tool/Liz.Tool/Program.cs | 163 +++++++- src/Tool/Liz.Tool/Properties/AssemblyInfo.cs | 4 +- .../Liz.Tool/Properties/launchSettings.json | 2 +- 35 files changed, 1528 insertions(+), 827 deletions(-) create mode 100644 src/Core/Liz.Core.Tests/Preparation/DeserializePackageExclusionGlobsPreprocessorTests.cs create mode 100644 src/Core/Liz.Core.Tests/Preparation/DeserializeProjectExclusionGlobsPreprocessorTests.cs create mode 100644 src/Core/Liz.Core.Tests/Result/ExportToJsonResultProcessorTests.cs create mode 100644 src/Core/Liz.Core/Preparation/DeserializePackageExclusionGlobsPreprocessor.cs create mode 100644 src/Core/Liz.Core/Preparation/DeserializeProjectExclusionGlobsPreprocessor.cs create mode 100644 src/Core/Liz.Core/Result/ExportToJsonResultProcessor.cs delete mode 100644 src/Tool/Liz.Tool.Tests/CommandLine/CommandProviderTests.cs delete mode 100644 src/Tool/Liz.Tool.Tests/CommandLine/CommandRunnerTests.cs delete mode 100644 src/Tool/Liz.Tool.Tests/Liz.Tool.Tests.csproj delete mode 100644 src/Tool/Liz.Tool/CommandLine/CommandProvider.cs delete mode 100644 src/Tool/Liz.Tool/CommandLine/CommandRunner.cs delete mode 100644 src/Tool/Liz.Tool/Contracts/CommandLine/ICommandRunner.cs diff --git a/README.md b/README.md index 5a7255f..221ce2e 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,14 @@ - Validate the determined package-references and their license-types against a provided whitelist/blacklist - Export license-information in various forms: - license-texts into text-files in a given directory + - all the gathered information into a single JSON-file +- Filtering + - Exclude projects by file-path using [glob-patterns](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns) + - Exclude packages by name using [glob-patterns](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns) ### Planned features - [#11](https://github.com/wgnf/liz/issues/11) & [#12](https://github.com/wgnf/liz/issues/12) Mapping from package-reference to license-information -- [#16](https://github.com/wgnf/liz/issues/16) Export license-information in various forms to a given directory/file -- [#5](https://github.com/wgnf/liz/issues/5) & [#7](https://github.com/wgnf/liz/issues/7) Filter for projects and dependencies - [#6](https://github.com/wgnf/liz/issues/6) Ability to provide manual dependencies - [#1](https://github.com/wgnf/liz/issues/1) Caching for even faster analyzation times - [#28](https://github.com/wgnf/liz/issues/28) Sanitize HTML-Tags diff --git a/doc/documenation-cake-addin.md b/doc/documenation-cake-addin.md index bd3e822..3412983 100644 --- a/doc/documenation-cake-addin.md +++ b/doc/documenation-cake-addin.md @@ -60,7 +60,12 @@ The settings contain the following options which can be set according to your ne | `LicenseTypeBlacklist` | A list of license-types, which are the only ones disallowed, when validating the determined license-types. Any license-type that is the same as within that blacklist will cause the validation to fail. Any other license-type is allowed.
This option is mutually exclusive with `LicenseTypeWhitelist` and `LicenseTypeWhitelistFilePath` | | `LicenseTypeBlacklistFilePath` | A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of license-types, which are the only ones disallowed, when validating the determined license-types. Any license-type that is the same as within that blacklist will cause the validation to fail. Any other license-type is allowed.
This option is mutually exclusive with `LicenseTypeWhitelist` and `LicenseTypeWhitelistFilePath`
If both `LicenseTypeBlacklist` and `LicenseTypeBlacklistFilePath` are given, those two will be merged | | `ExportLicenseTextsDirectory` | A path to a directory to where the determined license-texts will be exported
Each license-text will be written to an individual file with the file-name being: `-.txt`. If the license-text is the content of a website, the contents will be written into an ".html" file instead | +| `ExportJsonFile` | A path to a JSON-file to which the determined license- and package-information will be exported. All the information will be written to a single JSON-file.
If the file already exists it will be overwritten. | | `RequestTimeout` | The timeout for a request (i.e. to get the license text from a website).
After this amount of time a request will be considered as failed and aborted.
This defaults to 10 seconds | +| `ProjectExclusionGlobs` | A list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least one glob-pattern. The pattern will be matched against absolute path of the project-file.
All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns) | +| `ProjectExclusionGlobsFilePath` | A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least one glob-pattern The pattern will be matched against the absolute path of the project-file.
All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns)
If both `ProjectExclusionGlobs` and `ProjectExclusionGlobsFilePath` are given, those two will be merged. | +| `PackageExclusionGlobs` | A list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least one glob-pattern. The pattern will be matched against the name of the package.
All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns) | +| `PackageExclusionGlobsFilePath` | A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least one glob-pattern The pattern will be matched against the name of the package.
All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns)
If both `PackageExclusionGlobs` and `PackageExclusionGlobsFilePath` are given, those two will be merged. | ## Example Usages @@ -287,3 +292,35 @@ var settings = new ExtractLicensesSettings LicenseTypeBlacklistFilePath = "http://path/to/file.json" }; ``` + +#### Excluding projects + +If you want to for instance exclude all the test-projects when you're scanning a whole solution, you can use something like the following: + +```cs +var settings = new ExtractLicensesSettings +{ + ProjectExclusionGlobs = new + { + "*/**/*Tests.csproj" + } +} +``` + +when all your test-projects end with `Tests.csproj`. + +#### Excluding packages + +If you want to exclude certain packages, you can use something like the following: + +```cs +var settings = new ExtractLicensesSettings +{ + PackageExclusionGlobs = new + { + "YourCompany*" + } +} +``` + +When you i.e. want to exclude all packages from your company. diff --git a/doc/documentation-dotnet-tool.md b/doc/documentation-dotnet-tool.md index b8904c2..135a94c 100644 --- a/doc/documentation-dotnet-tool.md +++ b/doc/documentation-dotnet-tool.md @@ -79,7 +79,10 @@ To analyze a project your solution you have to use | `--whitelist`, `-w` | Provide a path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of license-types, which are the only ones allowed, when validating the determined license-types. Any license-type which is not in the whitelist will cause the validation to fail.
`--whitelist` and `--blacklist` are mutually exclusive! | | `--blacklist`, `-b` | Provide a path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of license-types, which are the only ones disallowed, when validating the determined license-types. Any license-type that is the same as within that blacklist will cause the validation to fail. Any other license-type is allowed.
`--whitelist` and `--blacklist` are mutually exclusive! | | `--export-texts`, `-et` | A path to a directory to where the determined license-texts will be exported.
Each license-text will be written to an individual file with the file-name being: `-.txt`. If the license-text is the content of a website, the contents will be written into an \".html\" file instead | +| `--export-json`, `-ej` | A path to a JSON-file to which the determined license- and package-information will be exported. All the information will be written to a single JSON-file.
If the file already exists it will be overwritten. | | `--timeout`, `-t` | The timeout for a request (i.e. to get the license text from a website) in **seconds**.
After this amount of time a request will be considered as failed and aborted.
This defaults to 10 seconds | +| `--project-excludes`, `-pe` | A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least one glob-pattern. The pattern will be matched against the absolute path of the project-file. All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns) | +| `--dependency-excludes`, `-de` | A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least one glob-pattern. The pattern will be matched against the name of the package. All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns) | ## Examples @@ -264,3 +267,63 @@ You can also use files that are stored remotely. Just use the web link to the re # local > dotnet liz "path/to/project.csproj" --blacklist "http://path/to/blacklist.json" ``` + +#### Excluding projects + +Create a JSON-File that contains your glob-patterns. If you want to exclude all your test-projects when you're scanning a whole solution, create a `project-excludes.json` (you can choose any other name of course) like this: + +```json +[ + "*/**/*Tests.csproj" +] +``` + +This will disallow every project whose file-name ends with `Tests.csproj`. You can then use it like this: + +```bash +# global +> liz "path/to/solution.sln" --project-excludes "path/to/project-excludes.json" + +# local +> dotnet liz "path/to/solution.sln" --project-excludes "path/to/project-excludes.json" +``` + +You can also use files that are stored remotely. Just usse the web link to the resource: + +```bash +# global +> liz "path/to/solution.sln" --project-excludes "http://path/to/project-excludes.json" + +# local +> dotnet liz "path/to/solution.sln" --project-excludes "http://path/to/project-excludes.json" +``` + +#### Excluding packages + +Create a JSON-File that contains your glob-patterns. If you want to exclude all the packages of your company, create a `package-excludes.json` (you can choose any other name of course) like this: + +```json +[ + "YourCompany*" +] +``` + +This will disallow every package whose name starts with "YourCompany". You can then use it like this: + +```bash +# global +> liz "path/to/project.csproj" --dependency-excludes "path/to/package-excludes.json" + +# local +> dotnet liz "path/to/project.csproj" --dependency-excludes "path/to/package-excludes.json" +``` + +You can also use files that are stored remotely. Just usse the web link to the resource: + +```bash +# global +> liz "path/to/project.csproj" --dependency-excludes "http://path/to/package-excludes.json" + +# local +> dotnet liz "path/to/project.csproj" --dependency-excludes "http://path/to/package-excludes.json" +``` diff --git a/doc/documentation-nuke-addon.md b/doc/documentation-nuke-addon.md index cd664f7..11a0405 100644 --- a/doc/documentation-nuke-addon.md +++ b/doc/documentation-nuke-addon.md @@ -43,7 +43,12 @@ The settings contain the following options which can be set according to your ne | `LicenseTypeBlacklist` | A list of license-types, which are the only ones disallowed, when validating the determined license-types. Any license-type that is the same as within that blacklist will cause the validation to fail. Any other license-type is allowed.
This option is mutually exclusive with `LicenseTypeWhitelist` and `LicenseTypeWhitelistFilePath` | | `LicenseTypeBlacklistFilePath` | A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of license-types, which are the only ones disallowed, when validating the determined license-types. Any license-type that is the same as within that blacklist will cause the validation to fail. Any other license-type is allowed.
This option is mutually exclusive with `LicenseTypeWhitelist` and `LicenseTypeWhitelistFilePath`
If both `LicenseTypeBlacklist` and `LicenseTypeBlacklistFilePath` are given, those two will be merged | | `ExportLicenseTextsDirectory` | A path to a directory to where the determined license-texts will be exported
Each license-text will be written to an individual file with the file-name being: `-.txt`. If the license-text is the content of a website, the contents will be written into an ".html" file instead | +| `ExportJsonFile` | A path to a JSON-file to which the determined license- and package-information will be exported. All the information will be written to a single JSON-file.
If the file already exists it will be overwritten. | | `RequestTimeout` | The timeout for a request (i.e. to get the license text from a website).
After this amount of time a request will be considered as failed and aborted.
This defaults to 10 seconds | +| `ProjectExclusionGlobs` | A list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least one glob-pattern. The pattern will be matched against absolute path of the project-file.
All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns) | +| `ProjectExclusionGlobsFilePath` | A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least one glob-pattern The pattern will be matched against the absolute path of the project-file.
All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns)
If both `ProjectExclusionGlobs` and `ProjectExclusionGlobsFilePath` are given, those two will be merged. | +| `PackageExclusionGlobs` | A list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least one glob-pattern. The pattern will be matched against the name of the package.
All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns) | +| `PackageExclusionGlobsFilePath` | A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing a list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least one glob-pattern The pattern will be matched against the name of the package.
All available patterns can be found [here](https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns)
If both `PackageExclusionGlobs` and `PackageExclusionGlobsFilePath` are given, those two will be merged. | To support the Nuke-specific way of configuring the settings in a Fluent-API way, following extensions were added as well: @@ -82,8 +87,15 @@ To support the Nuke-specific way of configuring the settings in a Fluent-API way | `SetLicenseTypeBlacklistFilePath` | Sets the `LicenseTypeBlacklistFilePath` property to the given value | | | | | `SetExportLicenseTextsDirectory` | Sets the `ExportLicenseTextsDirectory` property to the given value | +| `SetExportJsonFileDirectory` | Sets the `ExportJsonFile` property to the given value | | | | | `SetRequestTimeout` | Sets the `RequestTimeout` property to the given value | +| | | +| `SetProjectExclusionGlobs` | Sets the `ProjectExclusionGlobs` property to the given value | +| `SetProjectExclusionGlobsFilePath` | Sets the `ProjectExclusionGlobsFilePath` property to the given value | +| | | +| `SetPackageExclusionGlobs` | Sets the `PackageExclusionGlobs` property to the given value | +| `SetPackageExclusionGlobsFilePath` | Sets the `PackageExclusionGlobsFilePath` property to the given value | ## Example Usages @@ -264,3 +276,23 @@ await ExtractLicensesTasks.ExtractLicensesAsync(settings => settings await ExtractLicensesTasks.ExtractLicensesAsync(settings => settings .SetLicenseTypeBlacklistFilePath("http://path/to/file.json"); ``` + +#### Exluding projects + +If you want to for instance exclude all the test-projects when you're scanning a whole solution, you can use something like the following: + +```cs +// this will specifically disallow any project that ends with "Tests.csproj" +await ExtractLicensesTasks.ExtractLicensesAsync(settings => settings + .SetProjectExclusionGlobs(new List{ "*/**/*Tests.csproj" })); +``` + +#### Exluding packages + +If you want to for instance exclude all the packages of your company, you can use something like the following: + +```cs +// this will specifically disallow any project that ends with "Tests.csproj" +await ExtractLicensesTasks.ExtractLicensesAsync(settings => settings + .SetPackageExclusionGlobs(new List{ "YourCompany*" })); +``` diff --git a/src/Cake/Liz.Cake/Liz.Cake.csproj b/src/Cake/Liz.Cake/Liz.Cake.csproj index 2fc1ce4..93d7d7d 100644 --- a/src/Cake/Liz.Cake/Liz.Cake.csproj +++ b/src/Cake/Liz.Cake/Liz.Cake.csproj @@ -18,6 +18,7 @@ true true snupkg + true Martin Wagenführ diff --git a/src/Core/Liz.Core.IntegrationTests/GetProjectReferencesTests.cs b/src/Core/Liz.Core.IntegrationTests/GetProjectReferencesTests.cs index d4d19d8..4381a77 100644 --- a/src/Core/Liz.Core.IntegrationTests/GetProjectReferencesTests.cs +++ b/src/Core/Liz.Core.IntegrationTests/GetProjectReferencesTests.cs @@ -11,7 +11,7 @@ public class GetProjectReferencesTests [Fact] public void Gets_Project_References_Of_Liz_Tool_Project_Correctly() { - const string lizToolProjectPath = "../../../../../Tool/Liz.Tool.Tests/Liz.Tool.Tests.csproj"; + const string lizToolProjectPath = "../../../../../Tool/Liz.Tool/Liz.Tool.csproj"; var fileSystem = new FileSystem(); var lizToolProjectFile = fileSystem.FileInfo.FromFileName(lizToolProjectPath); diff --git a/src/Core/Liz.Core.Tests/Liz.Core.Tests.csproj b/src/Core/Liz.Core.Tests/Liz.Core.Tests.csproj index c881e8d..f14e8ea 100644 --- a/src/Core/Liz.Core.Tests/Liz.Core.Tests.csproj +++ b/src/Core/Liz.Core.Tests/Liz.Core.Tests.csproj @@ -6,16 +6,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,7 +23,7 @@ - + diff --git a/src/Core/Liz.Core.Tests/PackageReferences/GetPackageReferencesFacadeTests.cs b/src/Core/Liz.Core.Tests/PackageReferences/GetPackageReferencesFacadeTests.cs index 4ebe2d1..1b56766 100644 --- a/src/Core/Liz.Core.Tests/PackageReferences/GetPackageReferencesFacadeTests.cs +++ b/src/Core/Liz.Core.Tests/PackageReferences/GetPackageReferencesFacadeTests.cs @@ -5,6 +5,9 @@ using Liz.Core.Projects.Contracts.Models; using Moq; using System.IO.Abstractions; +using FluentAssertions; +using Liz.Core.PackageReferences.Contracts.Models; +using Liz.Core.Settings; using Xunit; namespace Liz.Core.Tests.PackageReferences; @@ -97,4 +100,47 @@ public async Task GetFromProject_And_Get_From_Packages_Config_On_Non_Sdk_Style_F .Verify(getPackage => getPackage.GetFromProjectAsync(project, includeTransitive), Times.Once); } + + [Fact] + public async Task GetFromProject_And_Exclude_Packages_According_To_Globs() + { + var settings = Mock.Of(); + settings.PackageExclusionGlobs.Add("Test*"); + + var context = new ArrangeContext(); + context.Use(settings); + + var sut = context.Build(); + + var packages = new[] + { + new PackageReference("Test", "net6.0", "1.2.3"), + new PackageReference("Test.Core", "net6.0", "1.2.3"), + new PackageReference("Test.Utils.Core", "net6.0", "1.2.3"), + new PackageReference("Testinson", "net6.0", "1.2.3"), + new PackageReference("Martin", "net6.0", "1.2.3") + }; + + context + .For() + .Setup(getPackageReferences => getPackageReferences.GetFromProjectAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(packages); + + var projectFileMock = new Mock(); + projectFileMock + .SetupGet(file => file.Exists) + .Returns(true); + + var project = new Project("Something", projectFileMock.Object, ProjectFormatStyle.SdkStyle); + + var result = (await sut.GetFromProjectAsync(project, true)).ToList(); + + result + .Should() + .HaveCount(1); + + result + .Should() + .OnlyContain(package => package.Name == "Martin"); + } } diff --git a/src/Core/Liz.Core.Tests/Preparation/DeserializePackageExclusionGlobsPreprocessorTests.cs b/src/Core/Liz.Core.Tests/Preparation/DeserializePackageExclusionGlobsPreprocessorTests.cs new file mode 100644 index 0000000..8c4cf88 --- /dev/null +++ b/src/Core/Liz.Core.Tests/Preparation/DeserializePackageExclusionGlobsPreprocessorTests.cs @@ -0,0 +1,157 @@ +using ArrangeContext.Moq; +using FluentAssertions; +using Liz.Core.Logging.Contracts; +using Liz.Core.Preparation; +using Liz.Core.Settings; +using Liz.Core.Utils.Contracts; +using Moq; +using Xunit; + +namespace Liz.Core.Tests.Preparation; + +public class DeserializePackageExclusionGlobsPreprocessorTests +{ + [Fact] + public async Task Does_Nothing_When_Setting_Not_Set() + { + var settings = Mock.Of(); + settings.PackageExclusionGlobsFilePath = null; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + await sut.PreprocessAsync(); + + context + .For() + .Verify(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task Logs_Warning_When_Get_Content_Fails() + { + var settings = Mock.Of(); + settings.PackageExclusionGlobsFilePath = "something"; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + context + .For() + .Setup(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny())) + .Throws(); + + await sut.PreprocessAsync(); + + context + .For() + .Verify(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Warning), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task Logs_Warning_When_Json_Cannot_Be_Deserialized() + { + const string json = @"{ ""gibberish"": ""something"" }"; + + var settings = Mock.Of(); + settings.PackageExclusionGlobsFilePath = "something"; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + context + .For() + .Setup(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny())) + .Returns(Task.FromResult(json)); + + await sut.PreprocessAsync(); + + context + .For() + .Verify(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Warning), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task Only_Uses_Correct_Entries() + { + const string json = @" +[ + """", + "" "", + ""something"" +]"; + + var settings = Mock.Of(); + settings.PackageExclusionGlobsFilePath = "something"; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + context + .For() + .Setup(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny())) + .Returns(Task.FromResult(json)); + + await sut.PreprocessAsync(); + + var expectedEntries = new[] { "something" }; + + settings + .PackageExclusionGlobs + .Should() + .BeEquivalentTo(expectedEntries); + } + + [Fact] + public async Task Adds_Entries_To_Already_Existing_Entries() + { + const string json = @" +[ + """", + "" "", + ""something"" +]"; + + const string alreadyExistingEntry = "else"; + + var settings = Mock.Of(); + + settings.PackageExclusionGlobs.Add(alreadyExistingEntry); + settings.PackageExclusionGlobsFilePath = "something"; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + context + .For() + .Setup(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny())) + .Returns(Task.FromResult(json)); + + await sut.PreprocessAsync(); + + var expectedEntries = new[] { "something", alreadyExistingEntry }; + + settings + .PackageExclusionGlobs + .Should() + .BeEquivalentTo(expectedEntries); + } +} diff --git a/src/Core/Liz.Core.Tests/Preparation/DeserializeProjectExclusionGlobsPreprocessorTests.cs b/src/Core/Liz.Core.Tests/Preparation/DeserializeProjectExclusionGlobsPreprocessorTests.cs new file mode 100644 index 0000000..9190d0b --- /dev/null +++ b/src/Core/Liz.Core.Tests/Preparation/DeserializeProjectExclusionGlobsPreprocessorTests.cs @@ -0,0 +1,157 @@ +using ArrangeContext.Moq; +using FluentAssertions; +using Liz.Core.Logging.Contracts; +using Liz.Core.Preparation; +using Liz.Core.Settings; +using Liz.Core.Utils.Contracts; +using Moq; +using Xunit; + +namespace Liz.Core.Tests.Preparation; + +public class DeserializeProjectExclusionGlobsPreprocessorTests +{ + [Fact] + public async Task Does_Nothing_When_Setting_Not_Set() + { + var settings = Mock.Of(); + settings.ProjectExclusionGlobsFilePath = null; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + await sut.PreprocessAsync(); + + context + .For() + .Verify(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task Logs_Warning_When_Get_Content_Fails() + { + var settings = Mock.Of(); + settings.ProjectExclusionGlobsFilePath = "something"; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + context + .For() + .Setup(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny())) + .Throws(); + + await sut.PreprocessAsync(); + + context + .For() + .Verify(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Warning), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task Logs_Warning_When_Json_Cannot_Be_Deserialized() + { + const string json = @"{ ""gibberish"": ""something"" }"; + + var settings = Mock.Of(); + settings.ProjectExclusionGlobsFilePath = "something"; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + context + .For() + .Setup(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny())) + .Returns(Task.FromResult(json)); + + await sut.PreprocessAsync(); + + context + .For() + .Verify(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Warning), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task Only_Uses_Correct_Entries() + { + const string json = @" +[ + """", + "" "", + ""something"" +]"; + + var settings = Mock.Of(); + settings.ProjectExclusionGlobsFilePath = "something"; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + context + .For() + .Setup(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny())) + .Returns(Task.FromResult(json)); + + await sut.PreprocessAsync(); + + var expectedEntries = new[] { "something" }; + + settings + .ProjectExclusionGlobs + .Should() + .BeEquivalentTo(expectedEntries); + } + + [Fact] + public async Task Adds_Entries_To_Already_Existing_Entries() + { + const string json = @" +[ + """", + "" "", + ""something"" +]"; + + const string alreadyExistingEntry = "else"; + + var settings = Mock.Of(); + + settings.ProjectExclusionGlobs.Add(alreadyExistingEntry); + settings.ProjectExclusionGlobsFilePath = "something"; + + var context = ArrangeContext.Create(); + context.Use(settings); + + var sut = context.Build(); + + context + .For() + .Setup(fileContentProvider => fileContentProvider.GetFileContentAsync(It.IsAny())) + .Returns(Task.FromResult(json)); + + await sut.PreprocessAsync(); + + var expectedEntries = new[] { "something", alreadyExistingEntry }; + + settings + .ProjectExclusionGlobs + .Should() + .BeEquivalentTo(expectedEntries); + } +} diff --git a/src/Core/Liz.Core.Tests/Projects/GetProjectsViaSlnParserTests.cs b/src/Core/Liz.Core.Tests/Projects/GetProjectsViaSlnParserTests.cs index e472134..ceef10b 100644 --- a/src/Core/Liz.Core.Tests/Projects/GetProjectsViaSlnParserTests.cs +++ b/src/Core/Liz.Core.Tests/Projects/GetProjectsViaSlnParserTests.cs @@ -12,6 +12,7 @@ namespace Liz.Core.Tests.Projects; +// TODO: the tests here involving file-system are literal garbage... public class GetProjectsViaSlnParserTests { [Fact] @@ -286,4 +287,82 @@ public void Should_Also_Return_Any_Non_Sdk_Style_Project_When_Starting_At_Sdk_St .Contain(project => project.Name == "anotherFile" && project.FormatStyle == ProjectFormatStyle.NonSdkStyle); } + + [Fact] + public void Should_Exclude_Projects_Based_On_Globs() + { + var settings = Mock.Of(); + // excluding fsproj projects + settings.ProjectExclusionGlobs.Add("*/**/*.fsproj"); + + var context = new ArrangeContext(); + context.Use(settings); + + var sut = context.Build(); + + const string targetFile = "something"; + + var targetFileMock = new Mock(); + targetFileMock + .Setup(file => file.Exists) + .Returns(true); + targetFileMock + .Setup(file => file.Extension) + .Returns(".sln"); + + context + .For() + .Setup(fileSystem => fileSystem.FileInfo.FromFileName(targetFile)) + .Returns(targetFileMock.Object); + + var existingFileFsProject = new FileInfo("some/file.fsproj"); + + var existingFsproj = new SolutionProject(Guid.NewGuid(), "SomeFsproj", Guid.NewGuid(), ProjectType.Test, + existingFileFsProject); + + var existingFileCsProject = new FileInfo("some/file.csproj"); + + var existingCsproj = new SolutionProject(Guid.NewGuid(), "SomeCsproj", Guid.NewGuid(), ProjectType.Test, + existingFileCsProject); + + var solution = new Mock(); + solution + .Setup(sln => sln.AllProjects) + .Returns(new IProject[] { existingFsproj, existingCsproj }); + + context + .For() + .Setup(fileSystem => fileSystem.File.Exists(It.Is(s => s == existingFileFsProject.FullName))) + .Returns(true); + + context + .For() + .Setup(fileSystem => fileSystem.File.Exists(It.Is(s => s == existingFileCsProject.FullName))) + .Returns(true); + + context + .For() + .Setup(slnParser => slnParser.Parse(It.IsAny())) + .Returns(solution.Object); + + context + .For() + .Setup(fileSystem => fileSystem.FileInfo.FromFileName(existingFileFsProject.FullName)) + .Returns(new FileSystem().FileInfo.FromFileName(existingFileFsProject.FullName)); + + context + .For() + .Setup(fileSystem => fileSystem.FileInfo.FromFileName(existingFileCsProject.FullName)) + .Returns(new FileSystem().FileInfo.FromFileName(existingFileCsProject.FullName)); + + var projects = sut.GetFromFile(targetFile).ToList(); + + projects + .Should() + .HaveCount(1); + + projects + .Should() + .ContainSingle(project => project.Name == existingCsproj.Name); + } } diff --git a/src/Core/Liz.Core.Tests/Result/ExportToJsonResultProcessorTests.cs b/src/Core/Liz.Core.Tests/Result/ExportToJsonResultProcessorTests.cs new file mode 100644 index 0000000..9a129c1 --- /dev/null +++ b/src/Core/Liz.Core.Tests/Result/ExportToJsonResultProcessorTests.cs @@ -0,0 +1,77 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using ArrangeContext.Moq; +using FluentAssertions; +using Liz.Core.PackageReferences.Contracts.Models; +using Liz.Core.Result; +using Liz.Core.Settings; +using Moq; +using Xunit; + +namespace Liz.Core.Tests.Result; + +public sealed class ExportToJsonResultProcessorTests +{ + [Fact] + public async Task Throws_On_Invalid_Parameter() + { + var context = ArrangeContext.Create(); + var sut = context.Build(); + + await Assert.ThrowsAsync(() => sut.ProcessResultsAsync(null!)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Does_Nothing_When_Setting_Not_Set(string? exportJsonFile) + { + var mockFileSystem = new MockFileSystem(); + var settings = Mock.Of(); + + var context = ArrangeContext.Create(); + context.Use(mockFileSystem); + context.Use(settings); + + settings.ExportJsonFile = exportJsonFile; + + var sut = context.Build(); + + var packageReference = new PackageReference("Something", "net5.0", "1.0.0"); + await sut.ProcessResultsAsync(new[] { packageReference }); + + mockFileSystem + .AllFiles + .Should() + .BeEmpty(); + } + + [Fact] + public async Task Writes_Package_References_To_Json_File() + { + var mockFileSystem = new MockFileSystem(); + var settings = Mock.Of(); + + var context = ArrangeContext.Create(); + context.Use(mockFileSystem); + context.Use(settings); + + const string jsonExportFilePath = @"./somewhere/export.json"; + settings.ExportJsonFile = jsonExportFilePath; + + mockFileSystem.AddFile(jsonExportFilePath, new MockFileData(string.Empty)); + + var sut = context.Build(); + + var packageReference = new PackageReference("Something", "net5.0", "1.0.0"); + await sut.ProcessResultsAsync(new[] { packageReference }); + + var jsonFile = mockFileSystem.GetFile(jsonExportFilePath); + + jsonFile + .TextContents + .Should() + .NotBeNullOrEmpty(); + } +} diff --git a/src/Core/Liz.Core/ExtractLicensesFactory.cs b/src/Core/Liz.Core/ExtractLicensesFactory.cs index 7169ba2..5437102 100644 --- a/src/Core/Liz.Core/ExtractLicensesFactory.cs +++ b/src/Core/Liz.Core/ExtractLicensesFactory.cs @@ -62,7 +62,7 @@ public IExtractLicenses Create( fileSystem, parsePackagesConfigFile); - var getPackageReferences = new GetPackageReferencesFacade(logger, getPackageReferencesDotnetCli, getPackageReferencesPackagesConfig); + var getPackageReferences = new GetPackageReferencesFacade(settings, logger, getPackageReferencesDotnetCli, getPackageReferencesPackagesConfig); var provideTemporaryDirectories = new ProvideTemporaryDirectories(settings, fileSystem); @@ -108,7 +108,9 @@ public IExtractLicenses Create( new PrintPackageIssuesResultProcessor(settings, logger), new ValidateLicenseTypesWhitelistResultProcessor(settings, logger), new ValidateLicenseTypesBlacklistResultProcessor(settings, logger), - new ExportLicenseTextsResultProcessor(settings, fileSystem) + // exporters... + new ExportLicenseTextsResultProcessor(settings, fileSystem, logger), + new ExportToJsonResultProcessor(settings, fileSystem, logger) }; var provideNugetCacheDirectories = new ProvideNugetCacheDirectories(cliToolExecutor, fileSystem); @@ -145,7 +147,9 @@ public IExtractLicenses Create( new DeserializeLicenseTypeDefinitionsPreprocessor(settings, logger, fileContentProvider), new DeserializeUrlToLicenseTypeMappingPreprocessor(settings, logger, fileContentProvider), new DeserializeLicenseTypeWhitelistPreprocessor(settings, logger, fileContentProvider), - new DeserializeLicenseTypeBlacklistPreprocessor(settings, logger, fileContentProvider) + new DeserializeLicenseTypeBlacklistPreprocessor(settings, logger, fileContentProvider), + new DeserializeProjectExclusionGlobsPreprocessor(settings, logger, fileContentProvider), + new DeserializePackageExclusionGlobsPreprocessor(settings, logger, fileContentProvider) }; var extractLicenses = new ExtractLicenses( diff --git a/src/Core/Liz.Core/License/EnrichPackageReferenceWithLicenseInformation.cs b/src/Core/Liz.Core/License/EnrichPackageReferenceWithLicenseInformation.cs index 9bb7c42..3cbaef5 100644 --- a/src/Core/Liz.Core/License/EnrichPackageReferenceWithLicenseInformation.cs +++ b/src/Core/Liz.Core/License/EnrichPackageReferenceWithLicenseInformation.cs @@ -36,6 +36,7 @@ public async Task EnrichAsync(PackageReference packageReference) return; } + _logger.LogDebug($"Enriching license-information for package {packageReference}"); var artifactDirectory = _fileSystem.DirectoryInfo.FromDirectoryName(packageReference.ArtifactDirectory); var licenseInformation = await GetLicenseInformationAsync(artifactDirectory).ConfigureAwait(false); diff --git a/src/Core/Liz.Core/Liz.Core.csproj b/src/Core/Liz.Core/Liz.Core.csproj index 2216525..0d31fa4 100644 --- a/src/Core/Liz.Core/Liz.Core.csproj +++ b/src/Core/Liz.Core/Liz.Core.csproj @@ -16,6 +16,7 @@ true true snupkg + true Martin Wagenführ @@ -30,7 +31,8 @@ - + + diff --git a/src/Core/Liz.Core/PackageReferences/GetPackageReferencesFacade.cs b/src/Core/Liz.Core/PackageReferences/GetPackageReferencesFacade.cs index a3af2fd..f28f185 100644 --- a/src/Core/Liz.Core/PackageReferences/GetPackageReferencesFacade.cs +++ b/src/Core/Liz.Core/PackageReferences/GetPackageReferencesFacade.cs @@ -1,10 +1,12 @@ -using Liz.Core.Logging; +using DotNet.Globbing; +using Liz.Core.Logging; using Liz.Core.Logging.Contracts; using Liz.Core.PackageReferences.Contracts; using Liz.Core.PackageReferences.Contracts.DotnetCli; using Liz.Core.PackageReferences.Contracts.Models; using Liz.Core.PackageReferences.Contracts.NuGetCli; using Liz.Core.Projects.Contracts.Models; +using Liz.Core.Settings; namespace Liz.Core.PackageReferences; @@ -12,13 +14,16 @@ internal sealed class GetPackageReferencesFacade : IGetPackageReferences { private readonly IGetPackageReferencesViaDotnetCli _getPackageReferencesViaDotnetCli; private readonly IGetPackageReferencesViaPackagesConfig _getPackageReferencesViaPackagesConfig; + private readonly ExtractLicensesSettingsBase _settings; private readonly ILogger _logger; public GetPackageReferencesFacade( + ExtractLicensesSettingsBase settings, ILogger logger, IGetPackageReferencesViaDotnetCli getPackageReferencesViaDotnetCli, IGetPackageReferencesViaPackagesConfig getPackageReferencesViaPackagesConfig) { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _getPackageReferencesViaDotnetCli = getPackageReferencesViaDotnetCli ?? throw new ArgumentNullException(nameof(getPackageReferencesViaDotnetCli)); @@ -26,13 +31,19 @@ public GetPackageReferencesFacade( ?? throw new ArgumentNullException(nameof(getPackageReferencesViaPackagesConfig)); } - public Task> GetFromProjectAsync(Project project, bool includeTransitive) + public async Task> GetFromProjectAsync(Project project, bool includeTransitive) { - if (project == null) throw new ArgumentNullException(nameof(project)); + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + if (!project.File.Exists) + { throw new FileNotFoundException($"Project file '{project.File}' does not exist"); + } - return project.FormatStyle switch + var packages = await (project.FormatStyle switch { ProjectFormatStyle.Unknown => throw new ArgumentException( $"Cannot determine Dependencies for project '{project.Name}' " + @@ -43,7 +54,10 @@ public Task> GetFromProjectAsync(Project project, _ => throw new NotSupportedException( $"Format-Style '{project.FormatStyle}' for project '{project.Name}' is not supported") - }; + }).ConfigureAwait(false); + + packages = HandleExclusions(packages); + return packages; } private async Task> GetFromSdkStyle(Project project, bool includeTransitive) @@ -67,4 +81,21 @@ private async Task> GetFromNonSdkStyle(Project pro .ConfigureAwait(false); return packageReferences; } + + private IEnumerable HandleExclusions(IEnumerable packageReferences) + { + // this basically filters out any package which matches a given exclusion-glob + var exclusionGlobs = _settings.PackageExclusionGlobs; + var filteredPackages = packageReferences.Where(packageReference => + { + return !exclusionGlobs.Exists(glob => + { + var globPattern = Glob.Parse(glob); + var isMatch = globPattern.IsMatch(packageReference.Name); + return isMatch; + }); + }); + + return filteredPackages; + } } diff --git a/src/Core/Liz.Core/Preparation/DeserializePackageExclusionGlobsPreprocessor.cs b/src/Core/Liz.Core/Preparation/DeserializePackageExclusionGlobsPreprocessor.cs new file mode 100644 index 0000000..8519251 --- /dev/null +++ b/src/Core/Liz.Core/Preparation/DeserializePackageExclusionGlobsPreprocessor.cs @@ -0,0 +1,60 @@ +using System.Text.Json; +using Liz.Core.Logging; +using Liz.Core.Logging.Contracts; +using Liz.Core.Preparation.Contracts; +using Liz.Core.Settings; +using Liz.Core.Utils.Contracts; + +namespace Liz.Core.Preparation; + +internal sealed class DeserializePackageExclusionGlobsPreprocessor : IPreprocessor +{ + private readonly ExtractLicensesSettingsBase _settings; + private readonly ILogger _logger; + private readonly IFileContentProvider _fileContentProvider; + + public DeserializePackageExclusionGlobsPreprocessor( + ExtractLicensesSettingsBase settings, + ILogger logger, + IFileContentProvider fileContentProvider) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileContentProvider = fileContentProvider ?? throw new ArgumentNullException(nameof(fileContentProvider)); + } + + + public async Task PreprocessAsync() + { + var targetFilePath = _settings.PackageExclusionGlobsFilePath; + if (string.IsNullOrWhiteSpace(targetFilePath)) + { + return; + } + + try + { + _logger.LogDebug($"Preprocess: getting package-exclusion-globs from file {targetFilePath}..."); + + var fileContent = await _fileContentProvider.GetFileContentAsync(targetFilePath).ConfigureAwait(false); + var packageExclusionGlobsFromFile = DeserializePackageExclusionGlobs(fileContent); + AddToSettings(packageExclusionGlobsFromFile); + } + catch (Exception exception) + { + _logger.LogWarning($"Preparing package-exclusion-globs file {targetFilePath} failed", exception); + } + } + + private static IEnumerable DeserializePackageExclusionGlobs(string fileContent) + { + var packageExclusionGlobs = JsonSerializer.Deserialize>(fileContent); + + return packageExclusionGlobs?.Where(entry => !string.IsNullOrWhiteSpace(entry)) ?? Enumerable.Empty(); + } + + private void AddToSettings(IEnumerable packageExclusionGlobsFromFile) + { + _settings.PackageExclusionGlobs.AddRange(packageExclusionGlobsFromFile); + } +} diff --git a/src/Core/Liz.Core/Preparation/DeserializeProjectExclusionGlobsPreprocessor.cs b/src/Core/Liz.Core/Preparation/DeserializeProjectExclusionGlobsPreprocessor.cs new file mode 100644 index 0000000..e2ef980 --- /dev/null +++ b/src/Core/Liz.Core/Preparation/DeserializeProjectExclusionGlobsPreprocessor.cs @@ -0,0 +1,60 @@ +using System.Text.Json; +using Liz.Core.Logging; +using Liz.Core.Logging.Contracts; +using Liz.Core.Preparation.Contracts; +using Liz.Core.Settings; +using Liz.Core.Utils.Contracts; + +namespace Liz.Core.Preparation; + +internal sealed class DeserializeProjectExclusionGlobsPreprocessor : IPreprocessor +{ + private readonly ExtractLicensesSettingsBase _settings; + private readonly ILogger _logger; + private readonly IFileContentProvider _fileContentProvider; + + public DeserializeProjectExclusionGlobsPreprocessor( + ExtractLicensesSettingsBase settings, + ILogger logger, + IFileContentProvider fileContentProvider) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileContentProvider = fileContentProvider ?? throw new ArgumentNullException(nameof(fileContentProvider)); + } + + + public async Task PreprocessAsync() + { + var targetFilePath = _settings.ProjectExclusionGlobsFilePath; + if (string.IsNullOrWhiteSpace(targetFilePath)) + { + return; + } + + try + { + _logger.LogDebug($"Preprocess: getting project-exclusion-globs from file {targetFilePath}..."); + + var fileContent = await _fileContentProvider.GetFileContentAsync(targetFilePath).ConfigureAwait(false); + var projectExclusionGlobsFromFile = DeserializeProjectExclusionGlobs(fileContent); + AddToSettings(projectExclusionGlobsFromFile); + } + catch (Exception exception) + { + _logger.LogWarning($"Preparing project-exclusion-globs file {targetFilePath} failed", exception); + } + } + + private static IEnumerable DeserializeProjectExclusionGlobs(string fileContent) + { + var projectExclusionGlobs = JsonSerializer.Deserialize>(fileContent); + + return projectExclusionGlobs?.Where(entry => !string.IsNullOrWhiteSpace(entry)) ?? Enumerable.Empty(); + } + + private void AddToSettings(IEnumerable projectExclusionGlobsFromFile) + { + _settings.ProjectExclusionGlobs.AddRange(projectExclusionGlobsFromFile); + } +} diff --git a/src/Core/Liz.Core/Projects/GetProjectsViaSlnParser.cs b/src/Core/Liz.Core/Projects/GetProjectsViaSlnParser.cs index 6331b36..d434eca 100644 --- a/src/Core/Liz.Core/Projects/GetProjectsViaSlnParser.cs +++ b/src/Core/Liz.Core/Projects/GetProjectsViaSlnParser.cs @@ -4,6 +4,7 @@ using SlnParser.Contracts; using System.IO.Abstractions; using System.Xml.Linq; +using DotNet.Globbing; namespace Liz.Core.Projects; @@ -29,24 +30,31 @@ public GetProjectsViaSlnParser( public IEnumerable GetFromFile(string targetFile) { if (string.IsNullOrWhiteSpace(targetFile)) + { throw new ArgumentException("Value cannot be null or whitespace.", nameof(targetFile)); + } var fileInfo = _fileSystem.FileInfo.FromFileName(targetFile); ValidateProvidedTargetFile(fileInfo); var projects = GetProjects(fileInfo); + projects = HandleExclusions(projects); return projects; } private static void ValidateProvidedTargetFile(IFileSystemInfo targetFile) { if (!targetFile.Exists) + { throw new FileNotFoundException($"The provided target-file '{targetFile.FullName}' could not be found"); + } if (targetFile.Extension is not (".sln" or ".csproj" or ".fsproj")) + { throw new ArgumentException( $"The provided target-file '{targetFile.FullName}' is not of the right type " + "(only 'sln', 'csproj' and 'fsproj' is supported"); + } } private IEnumerable GetProjects(IFileInfo targetFile) @@ -67,8 +75,10 @@ private IEnumerable GetProjectFromProjectFile(IFileInfo targetFile) var project = new Project(projectName, targetFile, DetermineProjectFormatStyle(targetFile.FullName)); if (!_settings.IncludeTransitiveDependencies || project.FormatStyle != ProjectFormatStyle.SdkStyle) + { return new[] { project }; - + } + /* * NOTE: * if 'include transitive' is activated and 'project' is SDK-Style we have to check if there are any project-references to @@ -149,4 +159,21 @@ private IEnumerable DetermineProjectReferencesNonSdkStyle(Project start return new Project(projectName, fileInfo, projectReferenceWithStyle.Style); }); } + + private IEnumerable HandleExclusions(IEnumerable projects) + { + // this basically filters out any project which matches a given exclusion-glob + var exclusionGlobs = _settings.ProjectExclusionGlobs; + var filteredProjects = projects.Where(project => + { + return !exclusionGlobs.Exists(glob => + { + var globPattern = Glob.Parse(glob); + var isMatch = globPattern.IsMatch(project.File.FullName); + return isMatch; + }); + }); + + return filteredProjects; + } } diff --git a/src/Core/Liz.Core/Result/ExportLicenseTextsResultProcessor.cs b/src/Core/Liz.Core/Result/ExportLicenseTextsResultProcessor.cs index 428e0e3..b68af1f 100644 --- a/src/Core/Liz.Core/Result/ExportLicenseTextsResultProcessor.cs +++ b/src/Core/Liz.Core/Result/ExportLicenseTextsResultProcessor.cs @@ -2,6 +2,8 @@ using Liz.Core.Result.Contracts; using Liz.Core.Settings; using System.IO.Abstractions; +using Liz.Core.Logging; +using Liz.Core.Logging.Contracts; namespace Liz.Core.Result; @@ -9,22 +11,34 @@ internal sealed class ExportLicenseTextsResultProcessor : IResultProcessor { private readonly ExtractLicensesSettingsBase _settings; private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; public ExportLicenseTextsResultProcessor( ExtractLicensesSettingsBase settings, - IFileSystem fileSystem) + IFileSystem fileSystem, + ILogger logger) { _settings = settings ?? throw new ArgumentNullException(nameof(settings)); _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task ProcessResultsAsync(IEnumerable packageReferences) { - if (packageReferences == null) throw new ArgumentNullException(nameof(packageReferences)); - if (string.IsNullOrWhiteSpace(_settings.ExportLicenseTextsDirectory)) return; + if (packageReferences == null) + { + throw new ArgumentNullException(nameof(packageReferences)); + } + + if (string.IsNullOrWhiteSpace(_settings.ExportLicenseTextsDirectory)) + { + return; + } var directory = EnsureDirectoryExists(); await WriteLicenseTextsToFilesAsync(directory, packageReferences).ConfigureAwait(false); + + _logger.LogInformation($"Exported license-texts to '{_settings.ExportLicenseTextsDirectory}'!"); } private IDirectoryInfo EnsureDirectoryExists() @@ -38,13 +52,18 @@ private IDirectoryInfo EnsureDirectoryExists() private async Task WriteLicenseTextsToFilesAsync(IFileSystemInfo directory, IEnumerable packageReferences) { foreach (var packageReference in packageReferences) + { await WriteLicenseTextToFileAsync(directory, packageReference).ConfigureAwait(false); + } } private async Task WriteLicenseTextToFileAsync(IFileSystemInfo directory, PackageReference packageReference) { var licenseText = packageReference.LicenseInformation.Text; - if (string.IsNullOrWhiteSpace(licenseText)) return; + if (string.IsNullOrWhiteSpace(licenseText)) + { + return; + } var fileExtension = licenseText.Contains("") ? "html" diff --git a/src/Core/Liz.Core/Result/ExportToJsonResultProcessor.cs b/src/Core/Liz.Core/Result/ExportToJsonResultProcessor.cs new file mode 100644 index 0000000..8f04fc1 --- /dev/null +++ b/src/Core/Liz.Core/Result/ExportToJsonResultProcessor.cs @@ -0,0 +1,65 @@ +using System.IO.Abstractions; +using System.Text.Json; +using Liz.Core.Logging; +using Liz.Core.Logging.Contracts; +using Liz.Core.PackageReferences.Contracts.Models; +using Liz.Core.Result.Contracts; +using Liz.Core.Settings; + +namespace Liz.Core.Result; + +internal sealed class ExportToJsonResultProcessor : IResultProcessor +{ + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly ExtractLicensesSettingsBase _settings; + + public ExportToJsonResultProcessor( + ExtractLicensesSettingsBase settings, + IFileSystem fileSystem, + ILogger logger) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task ProcessResultsAsync(IEnumerable packageReferences) + { + if (packageReferences == null) + { + throw new ArgumentNullException(nameof(packageReferences)); + } + + if (string.IsNullOrWhiteSpace(_settings.ExportJsonFile)) + { + return; + } + + var json = GetReferencesAsJson(packageReferences); + await WriteJsonToFile(json).ConfigureAwait(false); + + _logger.LogInformation($"Exported as JSON to '{_settings.ExportJsonFile}'!"); + } + + private static string GetReferencesAsJson(IEnumerable packageReferences) + { + var json = JsonSerializer.Serialize(packageReferences, new JsonSerializerOptions { WriteIndented = true }); + return json; + } + + private async Task WriteJsonToFile(string json) + { + var destinationFile = _fileSystem.FileInfo.FromFileName(_settings.ExportJsonFile); + + // this will basically 'overwrite' the file if it exists + if (destinationFile.Exists) + { + destinationFile.Delete(); + } + + // make sure the enclosing directory(s) exist + _fileSystem.Directory.CreateDirectory(destinationFile.Directory.FullName); + await _fileSystem.File.WriteAllTextAsync(destinationFile.FullName, json); + } +} diff --git a/src/Core/Liz.Core/Settings/ExtractLicensesSettingsBase.cs b/src/Core/Liz.Core/Settings/ExtractLicensesSettingsBase.cs index 1151ad5..33d2c04 100644 --- a/src/Core/Liz.Core/Settings/ExtractLicensesSettingsBase.cs +++ b/src/Core/Liz.Core/Settings/ExtractLicensesSettingsBase.cs @@ -120,6 +120,60 @@ public abstract class ExtractLicensesSettingsBase /// /// public string? LicenseTypeBlacklistFilePath { get; set; } + + /// + /// + /// A list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least + /// one glob-pattern. The pattern will be matched against absolute path of the project-file. + /// + /// + /// All available patterns can be found here: https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns + /// + /// + public List ProjectExclusionGlobs { get; set; } = new(); + + /// + /// + /// A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing + /// a list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least + /// one glob-pattern. The pattern will be matched against the absolute path of the project-file. + /// + /// + /// All available patterns can be found here: https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns + /// + /// + /// If both and are given, + /// those two will be merged. + /// + /// + public string? ProjectExclusionGlobsFilePath { get; set; } + + /// + /// + /// A list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least + /// one glob-pattern. The pattern will be matched against the name of the package. + /// + /// + /// All available patterns can be found here: https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns + /// + /// + public List PackageExclusionGlobs { get; set; } = new(); + + /// + /// + /// A path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing + /// a list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least + /// one glob-pattern. The pattern will be matched against the name of the package. + /// + /// + /// All available patterns can be found here: https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns + /// + /// + /// If both and are given, + /// those two will be merged. + /// + /// + public string? PackageExclusionGlobsFilePath { get; set; } /// /// @@ -133,6 +187,17 @@ public abstract class ExtractLicensesSettingsBase /// public string? ExportLicenseTextsDirectory { get; set; } + /// + /// + /// A path to a JSON-file to which the determined license- and package-information will be exported. + /// All the information will be written to a single JSON-file. + /// + /// + /// If the file already exists it will be overwritten. + /// + /// + public string? ExportJsonFile { get; set; } + /// /// /// The timeout for a request (i.e. to get the license text from a website). @@ -161,6 +226,7 @@ public void EnsureValidity() ValidateTargetFile(); ValidateWhitelistAndBlacklist(); ValidateExportTextsDirectory(); + ValidateExportJsonFile(); } private void ValidateTargetFile() @@ -168,18 +234,24 @@ private void ValidateTargetFile() var targetFile = GetTargetFile(); if (string.IsNullOrWhiteSpace(targetFile)) + { throw new SettingsInvalidException("The target-file cannot be null/empty/whitespace"); + } ValidatePath(targetFile, "TargetFile"); if (!File.Exists(targetFile)) + { throw new SettingsInvalidException($"The given target-file ('{targetFile}') does not exist"); + } var targetFileExtension = Path.GetExtension(targetFile); if (!targetFileExtension.Contains("csproj", StringComparison.InvariantCultureIgnoreCase) && !targetFileExtension.Contains("fsproj", StringComparison.InvariantCultureIgnoreCase) && !targetFileExtension.Contains("sln", StringComparison.InvariantCultureIgnoreCase)) + { throw new SettingsInvalidException($"The given target-file ('{targetFile}') is not a csproj, fsproj nor sln file"); + } } private void ValidateWhitelistAndBlacklist() @@ -187,14 +259,26 @@ private void ValidateWhitelistAndBlacklist() // this will ensure the mutual exclusivity of the license-type whitelist and blacklist if ((LicenseTypeWhitelist.Any() || !string.IsNullOrWhiteSpace(LicenseTypeWhitelistFilePath)) && (LicenseTypeBlacklist.Any() || !string.IsNullOrWhiteSpace(LicenseTypeBlacklistFilePath))) + { throw new SettingsInvalidException("License-type whitelist and blacklist are mutually exclusive. " + "You cannot use them together. Either use the whitelist or the blacklist."); + } } private void ValidateExportTextsDirectory() { if (!string.IsNullOrWhiteSpace(ExportLicenseTextsDirectory)) + { ValidatePath(ExportLicenseTextsDirectory, nameof(ExportLicenseTextsDirectory)); + } + } + + private void ValidateExportJsonFile() + { + if (!string.IsNullOrWhiteSpace(ExportJsonFile)) + { + ValidatePath(ExportJsonFile, nameof(ExportJsonFile)); + } } private void ValidatePath(string path, string settingsName) diff --git a/src/Liz.sln b/src/Liz.sln index 54db288..fcaad91 100644 --- a/src/Liz.sln +++ b/src/Liz.sln @@ -19,8 +19,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tool", "Tool", "{4854F7AC-5 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liz.Tool", "Tool\Liz.Tool\Liz.Tool.csproj", "{2DB78516-0810-4CD4-80E9-A532A4856586}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liz.Tool.Tests", "Tool\Liz.Tool.Tests\Liz.Tool.Tests.csproj", "{8D8AD946-3247-4A2C-9204-B87C40AED8A1}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liz.Build", "..\build\Liz.Build.csproj", "{2AE13D06-5045-41C2-83D3-3E5BF0337E9C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cake", "Cake", "{6F31F9C5-F627-437F-A59F-01C8E2738578}" @@ -61,18 +59,6 @@ Global {2DB78516-0810-4CD4-80E9-A532A4856586}.Release|x64.Build.0 = Release|Any CPU {2DB78516-0810-4CD4-80E9-A532A4856586}.Release|x86.ActiveCfg = Release|Any CPU {2DB78516-0810-4CD4-80E9-A532A4856586}.Release|x86.Build.0 = Release|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Debug|x64.ActiveCfg = Debug|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Debug|x64.Build.0 = Debug|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Debug|x86.ActiveCfg = Debug|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Debug|x86.Build.0 = Debug|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Release|Any CPU.Build.0 = Release|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Release|x64.ActiveCfg = Release|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Release|x64.Build.0 = Release|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Release|x86.ActiveCfg = Release|Any CPU - {8D8AD946-3247-4A2C-9204-B87C40AED8A1}.Release|x86.Build.0 = Release|Any CPU {B8621B57-8078-4D3D-8D1F-0DF7828991C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B8621B57-8078-4D3D-8D1F-0DF7828991C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8621B57-8078-4D3D-8D1F-0DF7828991C8}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -168,7 +154,6 @@ Global {B8621B57-8078-4D3D-8D1F-0DF7828991C8} = {44F58819-8B73-4BB8-8659-0E4754E55274} {7463BD33-CED2-4166-9DFB-30CE8D7D17FD} = {44F58819-8B73-4BB8-8659-0E4754E55274} {2DB78516-0810-4CD4-80E9-A532A4856586} = {4854F7AC-5D3F-4025-BE2F-FD3163D6EE42} - {8D8AD946-3247-4A2C-9204-B87C40AED8A1} = {4854F7AC-5D3F-4025-BE2F-FD3163D6EE42} {D94CD096-4354-4275-8C7F-876BA35B76EA} = {6F31F9C5-F627-437F-A59F-01C8E2738578} {658A4D1F-4870-41B7-B8BB-A7CE07283312} = {6F31F9C5-F627-437F-A59F-01C8E2738578} {A190D67B-B193-4BCF-89AF-E807D8D56802} = {331B06AF-A93F-4AA7-A277-4CB5A2204B1E} diff --git a/src/Nuke/Liz.Nuke/ExtractLicensesSettingsExtensions.cs b/src/Nuke/Liz.Nuke/ExtractLicensesSettingsExtensions.cs index 5519ce7..d8a23f4 100644 --- a/src/Nuke/Liz.Nuke/ExtractLicensesSettingsExtensions.cs +++ b/src/Nuke/Liz.Nuke/ExtractLicensesSettingsExtensions.cs @@ -21,8 +21,15 @@ public static class ExtractLicensesSettingsExtensions /// Thrown when is null public static ExtractLicensesSettings SetTargetFile(this ExtractLicensesSettings settings, AbsolutePath targetFile) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - if (targetFile == null) throw new ArgumentNullException(nameof(targetFile)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (targetFile == null) + { + throw new ArgumentNullException(nameof(targetFile)); + } settings.TargetFile = targetFile; return settings; @@ -38,8 +45,11 @@ public static ExtractLicensesSettings SetIncludeTransitiveDependencies( this ExtractLicensesSettings settings, bool value) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.IncludeTransitiveDependencies = value; return settings; } @@ -51,8 +61,11 @@ public static ExtractLicensesSettings SetIncludeTransitiveDependencies( /// The settings public static ExtractLicensesSettings EnableIncludeTransitiveDependencies(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.IncludeTransitiveDependencies = true; return settings; } @@ -64,8 +77,11 @@ public static ExtractLicensesSettings EnableIncludeTransitiveDependencies(this E /// The settings public static ExtractLicensesSettings DisableIncludeTransitiveDependencies(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.IncludeTransitiveDependencies = false; return settings; } @@ -77,8 +93,11 @@ public static ExtractLicensesSettings DisableIncludeTransitiveDependencies(this /// The settings public static ExtractLicensesSettings ToggleIncludeTransitiveDependencies(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.IncludeTransitiveDependencies = !settings.IncludeTransitiveDependencies; return settings; } @@ -90,8 +109,11 @@ public static ExtractLicensesSettings ToggleIncludeTransitiveDependencies(this E /// The settings public static ExtractLicensesSettings ResetIncludeTransitiveDependencies(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.IncludeTransitiveDependencies = new ExtractLicensesSettings().IncludeTransitiveDependencies; return settings; } @@ -107,8 +129,11 @@ public static ExtractLicensesSettings SetSuppressPrintDetails( this ExtractLicensesSettings settings, bool value) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintDetails = value; return settings; } @@ -120,8 +145,11 @@ public static ExtractLicensesSettings SetSuppressPrintDetails( /// The settings public static ExtractLicensesSettings EnableSuppressPrintDetails(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintDetails = true; return settings; } @@ -133,8 +161,11 @@ public static ExtractLicensesSettings EnableSuppressPrintDetails(this ExtractLic /// The settings public static ExtractLicensesSettings DisableSuppressPrintDetails(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintDetails = false; return settings; } @@ -146,8 +177,11 @@ public static ExtractLicensesSettings DisableSuppressPrintDetails(this ExtractLi /// The settings public static ExtractLicensesSettings ToggleSuppressPrintDetails(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintDetails = !settings.SuppressPrintDetails; return settings; } @@ -159,8 +193,11 @@ public static ExtractLicensesSettings ToggleSuppressPrintDetails(this ExtractLic /// The settings public static ExtractLicensesSettings ResetSuppressPrintDetails(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintDetails = new ExtractLicensesSettings().SuppressPrintDetails; return settings; } @@ -175,8 +212,11 @@ public static ExtractLicensesSettings SetSuppressPrintIssues( this ExtractLicensesSettings settings, bool value) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintIssues = value; return settings; } @@ -188,8 +228,11 @@ public static ExtractLicensesSettings SetSuppressPrintIssues( /// The settings public static ExtractLicensesSettings EnableSuppressPrintIssues(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintIssues = true; return settings; } @@ -201,8 +244,11 @@ public static ExtractLicensesSettings EnableSuppressPrintIssues(this ExtractLice /// The settings public static ExtractLicensesSettings DisableSuppressPrintIssues(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintIssues = false; return settings; } @@ -214,8 +260,11 @@ public static ExtractLicensesSettings DisableSuppressPrintIssues(this ExtractLic /// The settings public static ExtractLicensesSettings ToggleSuppressPrintIssues(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintIssues = !settings.SuppressPrintIssues; return settings; } @@ -227,8 +276,11 @@ public static ExtractLicensesSettings ToggleSuppressPrintIssues(this ExtractLice /// The settings public static ExtractLicensesSettings ResetSuppressPrintIssues(this ExtractLicensesSettings settings) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + settings.SuppressPrintIssues = new ExtractLicensesSettings().SuppressPrintIssues; return settings; } @@ -245,10 +297,17 @@ public static ExtractLicensesSettings SetLicenseTypeDefinitions( this ExtractLicensesSettings settings, List licenseTypeDefinitions) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - if (licenseTypeDefinitions == null) throw new ArgumentNullException(nameof(licenseTypeDefinitions)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (licenseTypeDefinitions == null) + { + throw new ArgumentNullException(nameof(licenseTypeDefinitions)); + } - settings.LicenseTypeDefinitions = licenseTypeDefinitions ?? throw new ArgumentNullException(nameof(licenseTypeDefinitions)); + settings.LicenseTypeDefinitions = licenseTypeDefinitions; return settings; } @@ -273,9 +332,15 @@ public static ExtractLicensesSettings SetLicenseTypeDefinitionsFilePath( this ExtractLicensesSettings settings, string licenseTypeDefinitionsFilePath) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + if (string.IsNullOrWhiteSpace(licenseTypeDefinitionsFilePath)) + { throw new ArgumentException("Value cannot be null or whitespace.", nameof(licenseTypeDefinitionsFilePath)); + } settings.LicenseTypeDefinitionsFilePath = licenseTypeDefinitionsFilePath; return settings; @@ -292,8 +357,15 @@ public static ExtractLicensesSettings SetUrlToLicenseTypeMapping( this ExtractLicensesSettings settings, Dictionary urlToLicenseTypeMapping) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - if (urlToLicenseTypeMapping == null) throw new ArgumentNullException(nameof(urlToLicenseTypeMapping)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (urlToLicenseTypeMapping == null) + { + throw new ArgumentNullException(nameof(urlToLicenseTypeMapping)); + } settings.UrlToLicenseTypeMapping = urlToLicenseTypeMapping; return settings; @@ -320,9 +392,15 @@ public static ExtractLicensesSettings SetUrlToLicenseTypeMappingFilePath( this ExtractLicensesSettings settings, string urlToLicenseTypeMappingFilePath) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + if (string.IsNullOrWhiteSpace(urlToLicenseTypeMappingFilePath)) + { throw new ArgumentException("Value cannot be null or whitespace.", nameof(urlToLicenseTypeMappingFilePath)); + } settings.UrlToLicenseTypeMappingFilePath = urlToLicenseTypeMappingFilePath; return settings; @@ -347,8 +425,15 @@ public static ExtractLicensesSettings SetLicenseTypeWhitelist( this ExtractLicensesSettings settings, List licenseTypeWhiteList) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - if (licenseTypeWhiteList == null) throw new ArgumentNullException(nameof(licenseTypeWhiteList)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (licenseTypeWhiteList == null) + { + throw new ArgumentNullException(nameof(licenseTypeWhiteList)); + } settings.LicenseTypeWhitelist = licenseTypeWhiteList; return settings; @@ -378,9 +463,15 @@ public static ExtractLicensesSettings SetLicenseTypeWhitelistFilePath( this ExtractLicensesSettings settings, string licenseTypeWhitelistFilePath) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + if (string.IsNullOrWhiteSpace(licenseTypeWhitelistFilePath)) + { throw new ArgumentException("Value cannot be null or whitespace.", nameof(licenseTypeWhitelistFilePath)); + } settings.LicenseTypeWhitelistFilePath = licenseTypeWhitelistFilePath; return settings; @@ -406,8 +497,15 @@ public static ExtractLicensesSettings SetLicenseTypeBlacklist( this ExtractLicensesSettings settings, List licenseTypeBlacklist) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); - if (licenseTypeBlacklist == null) throw new ArgumentNullException(nameof(licenseTypeBlacklist)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (licenseTypeBlacklist == null) + { + throw new ArgumentNullException(nameof(licenseTypeBlacklist)); + } settings.LicenseTypeBlacklist = licenseTypeBlacklist; return settings; @@ -424,7 +522,7 @@ public static ExtractLicensesSettings SetLicenseTypeBlacklist( /// This option is mutually exclusive with "LicenseTypeWhitelist" and "LicenseTypeWhitelistFilePath" /// /// - /// If both LicenseTypeBlacklist" and "LicenseTypeBlacklistFilePath" are given, those two will be merged + /// If both "LicenseTypeBlacklist" and "LicenseTypeBlacklistFilePath" are given, those two will be merged /// /// /// The settings to set the value on @@ -438,9 +536,15 @@ public static ExtractLicensesSettings SetLicenseTypeBlacklistFilePath( this ExtractLicensesSettings settings, string licenseTypeBlacklistFilePath) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + if (string.IsNullOrWhiteSpace(licenseTypeBlacklistFilePath)) + { throw new ArgumentException("Value cannot be null or whitespace.", nameof(licenseTypeBlacklistFilePath)); + } settings.LicenseTypeBlacklistFilePath = licenseTypeBlacklistFilePath; return settings; @@ -467,9 +571,15 @@ public static ExtractLicensesSettings SetExportLicenseTextsDirectory( this ExtractLicensesSettings settings, string exportLicenseTextsDirectory) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + if (string.IsNullOrWhiteSpace(exportLicenseTextsDirectory)) + { throw new ArgumentException("Value cannot be null or whitespace.", nameof(exportLicenseTextsDirectory)); + } settings.ExportLicenseTextsDirectory = exportLicenseTextsDirectory; return settings; @@ -494,9 +604,188 @@ public static ExtractLicensesSettings SetRequestTimeout( this ExtractLicensesSettings settings, TimeSpan requestTimeout) { - if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } settings.RequestTimeout = requestTimeout; return settings; } + + /// + /// + /// A path to a JSON-file to which the determined license- and package-information will be exported. + /// All the information will be written to a single JSON-file. + /// + /// + /// If the file already exists it will be overwritten. + /// + /// + /// The settings to set the value on + /// The value to set + /// The settings + /// Thrown when the parameter is null + /// + /// Thrown when the parameter is either null, empty or whitespace + /// + public static ExtractLicensesSettings SetExportJsonFile( + this ExtractLicensesSettings settings, + string exportJsonFile) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (string.IsNullOrWhiteSpace(exportJsonFile)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(exportJsonFile)); + } + + settings.ExportJsonFile = exportJsonFile; + return settings; + } + + /// + /// + /// Set a list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least + /// one glob-pattern. The pattern will be matched against absolute path of the project-file. + /// + /// + /// All available patterns can be found here: https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns + /// + /// + /// The settings to set the value on + /// The value to set + /// The settings + /// + /// Thrown when the parameters or are null + /// + public static ExtractLicensesSettings SetProjectExclusionGlobs( + this ExtractLicensesSettings settings, + List projectExclusionGlobs) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (projectExclusionGlobs == null) + { + throw new ArgumentNullException(nameof(projectExclusionGlobs)); + } + + settings.ProjectExclusionGlobs = projectExclusionGlobs; + return settings; + } + + /// + /// + /// Set a path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing + /// a list of glob-patterns to exclude certain projects. A project will be excluded when it matches at least + /// one glob-pattern. The pattern will be matched against the absolute path of the project-file. + /// + /// + /// All available patterns can be found here: https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns + /// + /// + /// If both "ProjectExclusionGlobs" and "ProjectExclusionGlobsFilePath" are given, those two will be merged. + /// + /// + /// The settings to set the value on + /// The value to set + /// The settings + /// Thrown when the parameter is null + /// + /// Thrown when the parameter is either null, empty or whitespace + /// + public static ExtractLicensesSettings SetProjectExclusionGlobsFilePath( + this ExtractLicensesSettings settings, + string projectExclusionGlobsFilePath) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (string.IsNullOrWhiteSpace(projectExclusionGlobsFilePath)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(projectExclusionGlobsFilePath)); + } + + settings.ProjectExclusionGlobsFilePath = projectExclusionGlobsFilePath; + return settings; + } + + /// + /// + /// Set a list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least + /// one glob-pattern. The pattern will be matched against the name of the package. + /// + /// + /// All available patterns can be found here: https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns + /// + /// + /// The settings to set the value on + /// The value to set + /// The settings + /// + /// Thrown when the parameters or are null + /// + public static ExtractLicensesSettings SetPackageExclusionGlobs( + this ExtractLicensesSettings settings, + List packageExclusionGlobs) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (packageExclusionGlobs == null) + { + throw new ArgumentNullException(nameof(packageExclusionGlobs)); + } + + settings.PackageExclusionGlobs = packageExclusionGlobs; + return settings; + } + + /// + /// + /// Set a path to a JSON-File (local or remote - remote will be downloaded automatically if available) containing + /// a list of glob-patterns to exclude certain packages. A package will be excluded when it matches at least + /// one glob-pattern. The pattern will be matched against the name of the package. + /// + /// + /// All available patterns can be found here: https://github.com/dazinator/DotNet.Glob/tree/3.1.3#patterns + /// + /// + /// If both "PackageExclusionGlobs" and "PackageExclusionGlobsFilePath" are given, those two will be merged. + /// + /// + /// The settings to set the value on + /// The value to set + /// The settings + /// Thrown when the parameter is null + /// + /// Thrown when the parameter is either null, empty or whitespace + /// + public static ExtractLicensesSettings SetPackageExclusionGlobsFilePath( + this ExtractLicensesSettings settings, + string packageExclusionGlobsFilePath) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (string.IsNullOrWhiteSpace(packageExclusionGlobsFilePath)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(packageExclusionGlobsFilePath)); + } + + settings.PackageExclusionGlobsFilePath = packageExclusionGlobsFilePath; + return settings; + } } diff --git a/src/Nuke/Liz.Nuke/Liz.Nuke.csproj b/src/Nuke/Liz.Nuke/Liz.Nuke.csproj index 719a8de..205ab64 100644 --- a/src/Nuke/Liz.Nuke/Liz.Nuke.csproj +++ b/src/Nuke/Liz.Nuke/Liz.Nuke.csproj @@ -16,6 +16,7 @@ true true snupkg + true Martin Wagenführ diff --git a/src/Tool/Liz.Tool.Tests/CommandLine/CommandProviderTests.cs b/src/Tool/Liz.Tool.Tests/CommandLine/CommandProviderTests.cs deleted file mode 100644 index a2d5738..0000000 --- a/src/Tool/Liz.Tool.Tests/CommandLine/CommandProviderTests.cs +++ /dev/null @@ -1,306 +0,0 @@ -using ArrangeContext.Moq; -using FluentAssertions; -using Liz.Core.Logging.Contracts; -using Liz.Tool.CommandLine; -using Xunit; - -namespace Liz.Tool.Tests.CommandLine; - -public sealed class CommandProviderTests -{ - [Fact] - public void Should_Provide_Root_Command() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - rootCommand - .Should() - .NotBeNull(); - rootCommand - .Description - .Should() - .NotBeNullOrWhiteSpace(); - } - - [Fact] - public void Provided_Root_Command_Should_Have_File_Input_Argument() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var argument = rootCommand.Arguments.FirstOrDefault(opt => opt.Name == "targetFile"); - Assert.NotNull(argument); - - argument? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - argument? - .ValueType - .Should() - .Be(); - } - - [Fact] - public void Provided_Root_Command_Should_Have_Log_Level_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "log-level"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--log-level", "-l" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_Include_Transitive_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "include-transitive"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--include-transitive", "-i" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_Suppress_Print_Details_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "suppress-print-details"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--suppress-print-details", "-sd" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_Suppress_Print_Issues_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "suppress-print-issues"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--suppress-print-issues", "-si" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_Suppress_Progressbar_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "suppress-progressbar"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--suppress-progressbar", "-sb" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_License_Type_Definitions_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "license-type-definitions"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--license-type-definitions", "-td" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_Url_To_License_Type_Mapping_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "url-type-mapping"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--url-type-mapping", "-um" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_License_Type_Whitelist_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "whitelist"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--whitelist", "-w" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_License_Type_Blacklist_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "blacklist"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--blacklist", "-b" }); - } - - [Fact] - public void Provided_Root_Command_Should_Have_Export_License_Texts_Directory_Option() - { - var sut = new ArrangeContext().Build(); - - var rootCommand = sut.Get(); - - var option = rootCommand.Options.FirstOrDefault(opt => opt.Name == "export-texts"); - Assert.NotNull(option); - - option? - .Description - .Should() - .NotBeNullOrWhiteSpace(); - - option? - .ValueType - .Should() - .Be(); - - option? - .Aliases - .Should() - .Contain(new[] { "--export-texts", "-et" }); - } -} diff --git a/src/Tool/Liz.Tool.Tests/CommandLine/CommandRunnerTests.cs b/src/Tool/Liz.Tool.Tests/CommandLine/CommandRunnerTests.cs deleted file mode 100644 index 3f2cfc2..0000000 --- a/src/Tool/Liz.Tool.Tests/CommandLine/CommandRunnerTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using ArrangeContext.Moq; -using FluentAssertions; -using Liz.Core; -using Liz.Core.Extract.Contracts; -using Liz.Core.Logging.Contracts; -using Liz.Core.Progress; -using Liz.Core.Settings; -using Liz.Tool.CommandLine; -using Liz.Tool.Contracts.CommandLine; -using Moq; -using Xunit; - -namespace Liz.Tool.Tests.CommandLine; - -public sealed class CommandRunnerTests -{ - [Fact] - public void Should_Have_Correct_Interface() - { - var sut = new ArrangeContext().Build(); - - sut - .Should() - .BeAssignableTo(); - } - - [Fact] - public async Task Should_Fail_To_Run_When_Invalid_Parameters() - { - var sut = new ArrangeContext().Build(); - - await Assert.ThrowsAsync(() => sut.RunAsync( - null!, - LogLevel.Information, - true, - true, - true, - true, - null, - null, - null, - null, - null, - null)); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_Forward_Execution_To_Extract_Licenses_With_Settings(bool booleanParameter) - { - var extractLicenses = new Mock(); - var extractLicensesFactory = new Mock(); - - extractLicensesFactory - .Setup(factory => factory.Create( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(extractLicenses.Object); - - var sut = new CommandRunner(extractLicensesFactory.Object); - - var fileInfo = new FileInfo("some/file.txt"); - - await sut.RunAsync( - fileInfo, - LogLevel.Information, - booleanParameter, - booleanParameter, - booleanParameter, - booleanParameter, - null, - null, - null, - null, - null, - null); - - extractLicenses.Verify(e => e.ExtractAsync(), Times.Once()); - } -} diff --git a/src/Tool/Liz.Tool.Tests/Liz.Tool.Tests.csproj b/src/Tool/Liz.Tool.Tests/Liz.Tool.Tests.csproj deleted file mode 100644 index 73ca923..0000000 --- a/src/Tool/Liz.Tool.Tests/Liz.Tool.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net6.0 - false - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - diff --git a/src/Tool/Liz.Tool/CommandLine/CommandProvider.cs b/src/Tool/Liz.Tool/CommandLine/CommandProvider.cs deleted file mode 100644 index 8323618..0000000 --- a/src/Tool/Liz.Tool/CommandLine/CommandProvider.cs +++ /dev/null @@ -1,196 +0,0 @@ -using Liz.Core.Logging.Contracts; -using Liz.Tool.Contracts.CommandLine; -using System.CommandLine; -using System.CommandLine.Binding; -using System.Diagnostics.CodeAnalysis; - -namespace Liz.Tool.CommandLine; - -internal sealed class CommandProvider -{ - private readonly ICommandRunner _commandRunner; - - public CommandProvider(ICommandRunner? commandRunner = null) - { - _commandRunner = commandRunner ?? new CommandRunner(); - } - - [ExcludeFromCodeCoverage] // running root-command cannot easily be tested - public RootCommand Get() - { - var (rootCommand, symbols) = PrepareRootCommand(); - - rootCommand.SetHandler(async ( - FileInfo targetFile, - LogLevel logLevel, - bool includeTransitive, - bool suppressPrintDetails, - bool suppressPrintIssues, - bool suppressProgressbar, - string? licenseTypeDefinitions, - string? urlToLicenseTypeMapping, - string? whitelist, - string? blacklist, - string? exportTexts, - int? requestTimeout) => - { - await _commandRunner.RunAsync( - targetFile, - logLevel, - includeTransitive, - suppressPrintDetails, - suppressPrintIssues, - suppressProgressbar, - licenseTypeDefinitions, - urlToLicenseTypeMapping, - whitelist, - blacklist, - exportTexts, - requestTimeout) - .ConfigureAwait(false); - }, symbols.ToArray()); - - return rootCommand; - } - - private static (RootCommand rootCommand, List symbols) PrepareRootCommand() - { - var rootCommand = new RootCommand("dotnet-tool to analyze the licenses of your project(s)"); - var symbols = new List(); - - var targetFileArgument = GetTargetFileArgument(); - rootCommand.AddArgument(targetFileArgument); - symbols.Add(targetFileArgument); - - var options = GetOptions().ToList(); - foreach (var option in options) rootCommand.AddOption(option); - symbols.AddRange(options); - return (rootCommand, symbols); - } - - private static IEnumerable