From 6bf461ddde334439ca0374551e8cbf66ebb18a5a Mon Sep 17 00:00:00 2001 From: Simon Ensslen Date: Wed, 13 Mar 2024 23:01:42 +0100 Subject: [PATCH] Add basic support for license expressions --- .github/workflows/action.yml | 10 +- .../allowedLicenses.json | 1 + .../ignorePackages.json | 1 + .../overwritePackageInformation.json | 2 + .../projectsToCheck.json | 1 + .../urlToLicenseMapping.json | 2 + NuGetUtility.sln | 30 +- ...eferenceContainingLicenseExpression.csproj | 11 + src/NuGetLicenseCore/NuGetLicenseCore.csproj | 1 + .../LicenseValidator/LicenseValidator.cs | 12 +- src/NuGetUtility/NuGetUtility.csproj | 4 + .../SpdxAndExpression.cs | 53 +++ .../SpdxExpression.cs | 42 ++ .../SpdxExpressionException.cs | 22 ++ .../SpdxExpressionParser.cs | 373 ++++++++++++++++++ .../SpdxLicenseExpression.cs | 56 +++ .../SpdxLicenseReference.cs | 47 +++ .../SpdxOrExpression.cs | 53 +++ .../SpdxParsingOptions.cs | 29 ++ .../SpdxScopedExpression.cs | 46 +++ .../SpdxWithExpression.cs | 53 +++ .../Tethys.SPDX.ExpressionParser.csproj | 10 + src/Tethys.SPDX.ExpressionParser/Token.cs | 51 +++ src/Tethys.SPDX.ExpressionParser/TokenType.cs | 56 +++ .../LicenseValidator/LicenseValidatorTest.cs | 187 +++++++++ ...ReferencedPackagesReaderIntegrationTest.cs | 2 +- 26 files changed, 1149 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/allowedLicenses.json create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/ignorePackages.json create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/overwritePackageInformation.json create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/projectsToCheck.json create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/urlToLicenseMapping.json create mode 100644 integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxAndExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxExpressionException.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxExpressionParser.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxLicenseExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxLicenseReference.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxOrExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxParsingOptions.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxScopedExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxWithExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/Tethys.SPDX.ExpressionParser.csproj create mode 100644 src/Tethys.SPDX.ExpressionParser/Token.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/TokenType.cs diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index a7863408..c464f519 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -87,7 +87,7 @@ jobs: strategy: matrix: targetFramework: [net6.0, net7.0, net8.0] - project: [App, Tests] + project: [App, Tests, ProjectWithReferenceContainingLicenseExpression] include: - targetFramework: net6.0 @@ -112,6 +112,9 @@ jobs: - name: restore run: dotnet restore -p:TargetFramework=${{ matrix.targetFramework }} + - name: build + run: dotnet build --configuration Release -f ${{ matrix.targetFramework }} --no-restore + - name: build run: dotnet publish ./src/NuGetLicenseCore/NuGetLicenseCore.csproj --configuration Release -o ./release -f ${{ matrix.targetFramework }} --no-restore @@ -131,7 +134,7 @@ jobs: runs-on: windows-latest strategy: matrix: - project: [App, Tests] + project: [App, Tests, ProjectWithReferenceContainingLicenseExpression] steps: - uses: actions/checkout@v4 @@ -151,6 +154,9 @@ jobs: $path = [System.IO.Path]::GetFullPath("./release"); echo "publish to path: $path" echo "path=$path" >> $env:GITHUB_OUTPUT + + - name: build + run: msbuild -t:rebuild -property:Configuration=Release - name: build run: msbuild ./src/NuGetLicenseFramework/NuGetLicenseFramework.csproj /t:Publish /p:configuration=Release /p:PublishDir=${{ steps.release_path.outputs.path }} diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/allowedLicenses.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/allowedLicenses.json new file mode 100644 index 00000000..790c6abc --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/allowedLicenses.json @@ -0,0 +1 @@ +["MIT","Apache-2.0","MS-EULA"] diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/ignorePackages.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/ignorePackages.json new file mode 100644 index 00000000..6628f6d6 --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/ignorePackages.json @@ -0,0 +1 @@ +["NETStandard.Library"] diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/overwritePackageInformation.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/overwritePackageInformation.json new file mode 100644 index 00000000..0d4f101c --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/overwritePackageInformation.json @@ -0,0 +1,2 @@ +[ +] diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/projectsToCheck.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/projectsToCheck.json new file mode 100644 index 00000000..8f8f5b59 --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/projectsToCheck.json @@ -0,0 +1 @@ +["./test/integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj"] diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/urlToLicenseMapping.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/urlToLicenseMapping.json new file mode 100644 index 00000000..2c63c085 --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/urlToLicenseMapping.json @@ -0,0 +1,2 @@ +{ +} diff --git a/NuGetUtility.sln b/NuGetUtility.sln index 910955a0..a4ecb3e6 100644 --- a/NuGetUtility.sln +++ b/NuGetUtility.sln @@ -31,9 +31,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SimpleCppProject", "tests\targets\SimpleCppProject\SimpleCppProject.vcxproj", "{380FBD90-2CF0-4F83-A58E-EB98CE2EAE15}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetLicenseCore", "src\NuGetLicenseCore\NuGetLicenseCore.csproj", "{FBA6622A-C9E3-4250-AB79-35F02CAD2419}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NuGetLicenseCore", "src\NuGetLicenseCore\NuGetLicenseCore.csproj", "{FBA6622A-C9E3-4250-AB79-35F02CAD2419}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetLicenseFramework", "src\NuGetLicenseFramework\NuGetLicenseFramework.csproj", "{DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NuGetLicenseFramework", "src\NuGetLicenseFramework\NuGetLicenseFramework.csproj", "{DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tethys.SPDX.ExpressionParser", "src\Tethys.SPDX.ExpressionParser\Tethys.SPDX.ExpressionParser.csproj", "{408005E7-D628-477C-A816-59AA4AD3E40C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration", "integration", "{FFB2826C-17A3-4C18-8FFE-A34AB51592F7}" + ProjectSection(SolutionItems) = preProject + integration\ProjectWithReferenceContainingLicenseExpression\ProjectWithReferenceContainingLicenseExpression.csproj = integration\ProjectWithReferenceContainingLicenseExpression\ProjectWithReferenceContainingLicenseExpression.csproj + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -224,6 +231,24 @@ Global {DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}.TestWindows|x64.Build.0 = Debug|Any CPU {DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}.TestWindows|x86.ActiveCfg = Debug|Any CPU {DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}.TestWindows|x86.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|x64.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|x64.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|x86.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|x86.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|Any CPU.Build.0 = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|x64.ActiveCfg = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|x64.Build.0 = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|x86.ActiveCfg = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|x86.Build.0 = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|Any CPU.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|Any CPU.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|x64.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|x64.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|x86.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -240,6 +265,7 @@ Global {380FBD90-2CF0-4F83-A58E-EB98CE2EAE15} = {FA92392F-D895-4D1E-A5ED-E6DC3C08223E} {FBA6622A-C9E3-4250-AB79-35F02CAD2419} = {D2AB2D00-1F48-487D-BFE0-99FDB4E071CC} {DE079B9C-B6BA-4D53-8B83-03D3CBD4027F} = {D2AB2D00-1F48-487D-BFE0-99FDB4E071CC} + {408005E7-D628-477C-A816-59AA4AD3E40C} = {D2AB2D00-1F48-487D-BFE0-99FDB4E071CC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {70887D40-0182-4C32-BFA1-B5A02E405F11} diff --git a/integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj b/integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj new file mode 100644 index 00000000..bf628b54 --- /dev/null +++ b/integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/NuGetLicenseCore/NuGetLicenseCore.csproj b/src/NuGetLicenseCore/NuGetLicenseCore.csproj index fcacf09c..eb14ac45 100644 --- a/src/NuGetLicenseCore/NuGetLicenseCore.csproj +++ b/src/NuGetLicenseCore/NuGetLicenseCore.csproj @@ -24,6 +24,7 @@ AnyCPU README.md A .net tool to print and validate the licenses of .net code. This tool supports .NET (Core), .NET Standard and .NET Framework projects. + NuGet;License diff --git a/src/NuGetUtility/LicenseValidator/LicenseValidator.cs b/src/NuGetUtility/LicenseValidator/LicenseValidator.cs index c2e57447..467efba0 100644 --- a/src/NuGetUtility/LicenseValidator/LicenseValidator.cs +++ b/src/NuGetUtility/LicenseValidator/LicenseValidator.cs @@ -9,6 +9,7 @@ using NuGetUtility.Wrapper.NuGetWrapper.Packaging; using NuGetUtility.Wrapper.NuGetWrapper.Packaging.Core; using NuGetUtility.Wrapper.NuGetWrapper.Versioning; +using Tethys.SPDX.ExpressionParser; namespace NuGetUtility.LicenseValidator { @@ -126,7 +127,8 @@ private void ValidateLicenseByMetadata(IPackageMetadata info, case LicenseType.Expression: case LicenseType.Overwrite: string licenseId = info.LicenseMetadata!.License; - if (IsLicenseValid(licenseId)) + SpdxExpression? licenseExpression = SpdxExpressionParser.Parse(licenseId, _ => true, _ => true); + if (IsValidLicenseExpression(licenseExpression)) { AddOrUpdateLicense(result, info, @@ -154,6 +156,14 @@ private void ValidateLicenseByMetadata(IPackageMetadata info, } } + private bool IsValidLicenseExpression(SpdxExpression? expression) => expression switch + { + SpdxAndExpression and => IsValidLicenseExpression(and.Left) && IsValidLicenseExpression(and.Right), + SpdxOrExpression or => IsValidLicenseExpression(or.Left) || IsValidLicenseExpression(or.Right), + SpdxWithExpression or SpdxLicenseExpression or SpdxLicenseReference => IsLicenseValid(expression.ToString()), + _ => false, + }; + private async Task ValidateLicenseByUrl(IPackageMetadata info, string context, ConcurrentDictionary result, diff --git a/src/NuGetUtility/NuGetUtility.csproj b/src/NuGetUtility/NuGetUtility.csproj index e45108f7..c5feed65 100644 --- a/src/NuGetUtility/NuGetUtility.csproj +++ b/src/NuGetUtility/NuGetUtility.csproj @@ -60,4 +60,8 @@ + + + + \ No newline at end of file diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxAndExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxAndExpression.cs new file mode 100644 index 00000000..968bf0fd --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxAndExpression.cs @@ -0,0 +1,53 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxAndExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// compound-expression "AND" compound-expression + + #region PUBLIC PROPERTIES + /// + /// Gets the left side of the expression. + /// + public SpdxExpression Left { get; } + + /// + /// Gets the right side of the expression. + /// + public SpdxExpression Right { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The left. + /// The right. + public SpdxAndExpression(SpdxExpression? left, SpdxExpression? right) + { + Left = left ?? throw new ArgumentNullException(nameof(left)); + Right = right ?? throw new ArgumentNullException(nameof(right)); + } // SpdxAndExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"{Left.ToString()} AND {Right.ToString()}"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxAndExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxExpression.cs new file mode 100644 index 00000000..6dc9d1e0 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxExpression.cs @@ -0,0 +1,42 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +namespace Tethys.SPDX.ExpressionParser +{ + /************************************************************************* + * SPDX Expressions + * ---------------- + * idstring = 1*(ALPHA / DIGIT / "-" / "." ) + * + * license-id = + * + * license-exception-id = + * + * license-ref = ["DocumentRef-"(idstring)":"]"LicenseRef-"(idstring) + * + * simple-expression = license-id / license-id"+" / license-ref + * + * compound-expression = (simple-expression / + * simple-expression "WITH" license-exception-id / + * compound-expression "AND" compound-expression / + * compound-expression "OR" compound-expression / + * "(" compound-expression ")" ) + * + * license-expression = (simple-expression / compound-expression) + * + ************************************************************************/ + + /// + /// Represents an SPDX expression. + /// + public abstract class SpdxExpression + { + /// + /// Converts an to a string. + /// + /// + /// A that represents this instance. + /// + public new abstract string ToString(); + } // SpdxExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxExpressionException.cs b/src/Tethys.SPDX.ExpressionParser/SpdxExpressionException.cs new file mode 100644 index 00000000..3097db1b --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxExpressionException.cs @@ -0,0 +1,22 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxExpressionException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public SpdxExpressionException(string message) + : base(message) + { + } // SpdxExpressionException() + } // SpdxExpressionException +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxExpressionParser.cs b/src/Tethys.SPDX.ExpressionParser/SpdxExpressionParser.cs new file mode 100644 index 00000000..8e7f1132 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxExpressionParser.cs @@ -0,0 +1,373 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /************************************************************************* + * SPDX Expressions + * ---------------- + * idstring = 1*(ALPHA / DIGIT / "-" / "." ) + * + * license-id = + * + * license-exception-id = + * + * license-ref = ["DocumentRef-"(idstring)":"]"LicenseRef-"(idstring) + * + * simple-expression = license-id / license-id"+" / license-ref + * + * compound-expression = (simple-expression / + * simple-expression "WITH" license-exception-id / + * compound-expression "AND" compound-expression / + * compound-expression "OR" compound-expression / + * "(" compound-expression ")" ) + * + * license-expression = (simple-expression / compound-expression) + * + ************************************************************************/ + + /// + /// Represents an SPDX expression. + /// + public class SpdxExpressionParser + { + #region PRIVATE PROPERTIES + private readonly string[] _tokens; + private int _position = 0; + private readonly Func _isSpdxIdentifier; + private readonly Func _isSpdxException; + private readonly SpdxParsingOptions _options; + #endregion // PRIVATE PROPERTIES + + //// --------------------------------------------------------------------- + + /// + /// Parses a SPDX expression. + /// + /// The expression. + /// The is identifier. + /// The is exception. + /// The options. + /// + /// A . + /// + /// + /// Exception for parsing problems. + /// + public static SpdxExpression? Parse( + string expression, + Func? isIdentifier, + Func? isException, + SpdxParsingOptions parsingOptions = SpdxParsingOptions.Default) + { + if (string.IsNullOrEmpty(expression)) + { + throw new ArgumentNullException(nameof(expression)); + } // if + + // ensure that we detect all parenthesis + expression = expression.Replace("(", " ( "); + expression = expression.Replace(")", " ) "); + + var parser = new SpdxExpressionParser(expression, + isIdentifier ?? throw new ArgumentNullException(nameof(isIdentifier)), + isException ?? throw new ArgumentNullException(nameof(isException)), + parsingOptions); + + return parser.Parse(); + } // Parse() + + private SpdxExpressionParser(string expression, + Func isIdentifier, + Func isException, + SpdxParsingOptions parsingOptions) + { + _isSpdxIdentifier = isIdentifier; + _isSpdxException = isException; + _options = parsingOptions; + + // very much simplified ... + _tokens = expression.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + _position = -1; + } + + private SpdxExpression? Parse() + { + _ = GetNextToken() ?? throw new SpdxExpressionException(string.Empty); + SpdxExpression? expr = ParseAnd(); + + return expr; + } + + /// + /// Parses an "and" expression. + /// + /// A . + private SpdxExpression? ParseAnd() + { + SpdxExpression? expression = ParseOr(); + Token? currentToken = GetCurrentToken(); + while (currentToken?.Type == TokenType.And) + { + GetNextToken(); + expression = new SpdxAndExpression(expression, ParseOr()); + currentToken = GetCurrentToken(); + } // while + + return expression; + } // ParseAnd() + + /// + /// Parses an "or" expression. + /// + /// A . + private SpdxExpression? ParseOr() + { + SpdxExpression? expression; + Token? currentToken = GetCurrentToken(); + if (currentToken?.Type == TokenType.Left) + { + expression = ParseScopedExpression(); + } + else + { + expression = ParseLicense(); + } // if + + currentToken = GetCurrentToken(); + while (currentToken?.Type == TokenType.Or) + { + GetNextToken(); + expression = new SpdxOrExpression(expression, ParseOr()); + currentToken = GetCurrentToken(); + } // while + + return expression; + } // ParseOr() + + /// + /// Parses a scoped expression. + /// + /// A . + private SpdxExpression ParseScopedExpression() + { + GetNextToken(); + SpdxExpression? expression = ParseAnd(); + if (GetCurrentToken()?.Type != TokenType.Right) + { + throw new SpdxExpressionException("Unexpected end of expression."); + } // if + + GetNextToken(); + + return new SpdxScopedExpression(expression); + } // ParseScopedExpression() + + /// + /// Determines whether this expression contains invalid characters. + /// Allowed are (ALPHA / DIGIT / "-" / "." ). + /// + /// The expression. + /// + /// true if this expression contains invalid characters; otherwise, false. + /// + private static bool ContainsInvalidCharacters(string expression) + { + foreach (char c in expression) + { + if (c != '+' && c != '.' && c != '-' && c != '(' && c != ')' && !char.IsDigit(c) && !char.IsLetter(c)) + { + return true; + } // if + } // foreach + + return false; + } // ContainsInvalidCharacters() + + /// + /// Parses a license. + /// + /// A . + private SpdxExpression? ParseLicense() + { + Token? token = GetCurrentToken(); + if (token?.Type == TokenType.LicenseId) + { + if ((_options & SpdxParsingOptions.AllowUnknownLicenses) == 0 + && !_isSpdxIdentifier(token.Value.TrimEnd('+'))) + { + throw new SpdxExpressionException("Invalid/unknown SPDX license id"); + } // if + + Token? tokenNext = PeekNextToken(); + if (tokenNext?.Type == TokenType.With) + { + Token? t2 = PeekNextNextToken(); + if (t2?.Type == TokenType.Exception) + { + GetNextToken(); + GetNextToken(); + GetNextToken(); + + return new SpdxWithExpression( + GetLicenseExpression(token.Value), + t2.Value); + } // if + } // if + + GetNextToken(); + + return GetLicenseExpression(token.Value); + } // if + + if (token?.Type == TokenType.LicenseRef) + { + GetNextToken(); + return new SpdxLicenseReference(token.Value); + } // if + + return null; + } // ParseLicense() + + /// + /// Gets a license expression from the given string. + /// + /// The expression. + /// A . + private static SpdxLicenseExpression GetLicenseExpression(string expression) + { + if (expression.EndsWith("+")) + { + return new SpdxLicenseExpression(expression.TrimEnd('+'), true); + } // if + + return new SpdxLicenseExpression(expression, false); + } // GetLicenseExpression() + + /// + /// Gets a token from the given text. + /// + /// The text. + /// A . + private Token GetToken(string text) + { + string textCompare = text.Trim().ToLower(); + if (textCompare == "(") + { + return new Token(TokenType.Left, string.Empty); + } // if + + if (textCompare == ")") + { + return new Token(TokenType.Right, string.Empty); + } // if + + if (textCompare == "and") + { + return new Token(TokenType.And, "AND"); + } // if + + if (textCompare == "or") + { + return new Token(TokenType.Or, "OR"); + } // if + + if (textCompare.Contains("with")) + { + return new Token(TokenType.With, text); + } // if + + if (textCompare.StartsWith("licenseref")) + { + return new Token(TokenType.LicenseRef, text); + } // if + + if (textCompare.EndsWith("+")) + { + return new Token(TokenType.LicenseId, text); + } // if + + if (_isSpdxIdentifier(textCompare)) + { + return new Token(TokenType.LicenseId, text); + } // if + + if (_isSpdxException(textCompare)) + { + return new Token(TokenType.Exception, text); + } // if + + if (ContainsInvalidCharacters(textCompare)) + { + throw new SpdxExpressionException("Invalid characters found"); + } // if + + if ((_options & SpdxParsingOptions.AllowUnknownExceptions) != 0) + { + return new Token(TokenType.Exception, text); + } // if + + throw new SpdxExpressionException($"Unknown token: {text}"); + } // GetToken() + + /// + /// Gets the current token. + /// + /// A . + private Token? GetCurrentToken() + { + if (_position < _tokens.Length) + { + return GetToken(_tokens[_position]); + } // if + + return null; + } + + /// + /// Gets the next token. + /// + /// A or null. + private Token? GetNextToken() + { + if (_position < _tokens.Length - 1) + { + return GetToken(_tokens[++_position]); + } // if + + return null; + } // GetNextToken() + + /// + /// Peeks the next token. + /// + /// A or null. + private Token? PeekNextToken() + { + if (_position < _tokens.Length - 1) + { + return GetToken(_tokens[_position + 1]); + } // if + + return null; + } // PeekNextToken() + + /// + /// Peeks the next token. + /// + /// + /// A or null. + /// + private Token? PeekNextNextToken() + { + if (_position < _tokens.Length - 2) + { + return GetToken(_tokens[_position + 2]); + } // if + + return null; + } // PeekNextNextToken() + } // SpdxExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxLicenseExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxLicenseExpression.cs new file mode 100644 index 00000000..ca2c9fed --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxLicenseExpression.cs @@ -0,0 +1,56 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxLicenseExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// simple-expression = license-id / license-id"+" / license-ref + //// we replace this with + //// simple-expression = SpdxLicenseExpression / SpdxLicenseReference + + #region PUBLIC PROPERTIES + /// + /// Gets the license ID. + /// + public string Id { get; } + + /// + /// Gets a value indicating whether or not later versions of the license is accepted. + /// + public bool OrLater { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The identifier. + /// if set to true [or later]. + public SpdxLicenseExpression(string id, bool orLater) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + OrLater = orLater; + } // SpdxLicenseExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + string plus = OrLater ? "+" : string.Empty; + return $"{Id}{plus}"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxLicenseExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxLicenseReference.cs b/src/Tethys.SPDX.ExpressionParser/SpdxLicenseReference.cs new file mode 100644 index 00000000..6684adb9 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxLicenseReference.cs @@ -0,0 +1,47 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxLicenseReference : SpdxExpression + { + //// Annex D SPDX license expressions + //// LicenseRef-"(idstring) + + #region PUBLIC PROPERTIES + /// + /// Gets the license reference. + /// + public string LicenseRef { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The license reference. + public SpdxLicenseReference(string licenseRef) + { + LicenseRef = licenseRef ?? throw new ArgumentNullException(); + } // SpdxLicenseReference() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + string text = $"{LicenseRef}"; + return text; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxLicenseReference +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxOrExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxOrExpression.cs new file mode 100644 index 00000000..e76e86a1 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxOrExpression.cs @@ -0,0 +1,53 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxOrExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// compound-expression "OR" compound-expression + + #region PUBLIC PROPERTIES + /// + /// Gets the left side of the expression. + /// + public SpdxExpression Left { get; } + + /// + /// Gets the right side of the expression. + /// + public SpdxExpression Right { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The left. + /// The right. + public SpdxOrExpression(SpdxExpression? left, SpdxExpression? right) + { + Left = left ?? throw new ArgumentNullException(nameof(left)); + Right = right ?? throw new ArgumentNullException(nameof(right)); + } // SpdxOrExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"{Left.ToString()} OR {Right.ToString()}"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxOrExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxParsingOptions.cs b/src/Tethys.SPDX.ExpressionParser/SpdxParsingOptions.cs new file mode 100644 index 00000000..4a330cc9 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxParsingOptions.cs @@ -0,0 +1,29 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// SPDX parsing options. + /// + [Flags] + public enum SpdxParsingOptions + { + /// + /// Default (= no) option. + /// + Default = 0x00, + + /// + /// Allow unknown licenses. + /// + AllowUnknownLicenses = 0x01, + + /// + /// Allow unknown exceptions. + /// + AllowUnknownExceptions = 0x02, + } // SpdxParsingOptions +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxScopedExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxScopedExpression.cs new file mode 100644 index 00000000..cc8cafcf --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxScopedExpression.cs @@ -0,0 +1,46 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression enclosed by parenthesis. + /// + public class SpdxScopedExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// "(" compound-expression ")" + + #region PUBLIC PROPERTIES + /// + /// Gets the expression node. + /// + public SpdxExpression Expression { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The expression node. + public SpdxScopedExpression(SpdxExpression? expression) + { + Expression = expression ?? throw new ArgumentNullException(nameof(expression)); + } // SpdxScopedExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"({Expression.ToString()})"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxScopedExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxWithExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxWithExpression.cs new file mode 100644 index 00000000..91b887f6 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxWithExpression.cs @@ -0,0 +1,53 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxWithExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// simple-expression "WITH" license-exception-id + + #region PUBLIC PROPERTIES + /// + /// Gets the expression node. + /// + public SpdxExpression Expression { get; } + + /// + /// Gets the license exception node. + /// + public string Exception { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The expression node. + /// The license exception node. + public SpdxWithExpression(SpdxExpression expression, string exception) + { + Expression = expression ?? throw new ArgumentNullException(nameof(expression)); + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + } // SpdxWithExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"{Expression.ToString()} WITH {Exception}"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/Tethys.SPDX.ExpressionParser.csproj b/src/Tethys.SPDX.ExpressionParser/Tethys.SPDX.ExpressionParser.csproj new file mode 100644 index 00000000..4205133c --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/Tethys.SPDX.ExpressionParser.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + enable + SPDX Expression Parser + SPDX + 9.0 + + diff --git a/src/Tethys.SPDX.ExpressionParser/Token.cs b/src/Tethys.SPDX.ExpressionParser/Token.cs new file mode 100644 index 00000000..378e037b --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/Token.cs @@ -0,0 +1,51 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Implements a token. + /// + internal class Token + { + #region PUBLIC PROPERTIES + + /// + /// Gets or sets the type. + /// + public TokenType Type { get; set; } + + /// + /// Gets the value. + /// + public string Value { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The value. + public Token(TokenType type, string value) + { + Type = type; + Value = value ?? throw new ArgumentNullException(nameof(value)); + } // Token() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"{Type}: {Value}"; + } // ToString() + #endregion // PUBLIC METHODS + } // Token +} diff --git a/src/Tethys.SPDX.ExpressionParser/TokenType.cs b/src/Tethys.SPDX.ExpressionParser/TokenType.cs new file mode 100644 index 00000000..a6274815 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/TokenType.cs @@ -0,0 +1,56 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// The token types. + /// + internal enum TokenType + { + /// + /// A license identifier like MIT, Apache-2.0 or GPL-2.0. + /// + LicenseId, + + /// + /// A license reference like LicenseRed-someorg-somename. + /// + LicenseRef, + + /// + /// A license exception like Autoconf-exception-2.0. + /// + Exception, + + /// + /// A trailing plus sign to indicate "or later". + /// + Plus, + + /// + /// A left parenthesis. + /// + Left, + + /// + /// A right parenthesis. + /// + Right, + + /// + /// The license exception combination keyword.. + /// + With, + + /// + /// The license conjunction keyword. + /// + And, + + /// + /// The license disjunction keyword. + /// + Or, + } // TokenType +} diff --git a/tests/NuGetUtility.Test/LicenseValidator/LicenseValidatorTest.cs b/tests/NuGetUtility.Test/LicenseValidator/LicenseValidatorTest.cs index 22636dc4..5f5eefc6 100644 --- a/tests/NuGetUtility.Test/LicenseValidator/LicenseValidatorTest.cs +++ b/tests/NuGetUtility.Test/LicenseValidator/LicenseValidatorTest.cs @@ -307,6 +307,68 @@ public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_Give .Using(new LicenseValidationResultValueEqualityComparer())); } + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectValidatedLicenseList_When_Or_Expression( + string packageId, + INuGetVersion packageVersion, + string license1, + string license2) + { + _uut = new NuGetUtility.LicenseValidator.LicenseValidator(_licenseMapping, + Array.Empty(), + _fileDownloader, + _ignoredLicenses); + + string expression = $"{license1} OR {license2}"; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectValidatedLicenseList_When_And_Expression( + string packageId, + INuGetVersion packageVersion, + string license1, + string license2) + { + _uut = new NuGetUtility.LicenseValidator.LicenseValidator(_licenseMapping, + Array.Empty(), + _fileDownloader, + _ignoredLicenses); + + string expression = $"{license1} AND {license2}"; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + [Test] [ExtendedAutoData(typeof(NuGetVersionBuilder))] public async Task ValidatingLicensesWithOverwriteLicenseInformation_Should_GiveCorrectValidatedLicenseList( @@ -502,6 +564,80 @@ public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_Give .Using(new LicenseValidationResultValueEqualityComparer())); } + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectResult_WithOrExpression_If_NoneAllowed( + string packageId, + INuGetVersion packageVersion, + string[] licenses) + { + string expression = licenses.Length switch + { + 0 => string.Empty, + 1 => licenses[0], + 2 => $"{licenses[0]} OR {licenses[1]}", + _ => licenses.Skip(2).Aggregate($"{licenses[0]} OR {licenses[1]}", (expression, newLicense) => $"{newLicense} OR ({expression})") + }; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression, + new List + { + new ValidationError($"License {expression} not found in list of supported licenses", + _context) + }) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectResult_WithAndExpression_If_OneNotAllowed( + string packageId, + INuGetVersion packageVersion, + string unallowedLicense) + { + string[] licenses = _allowedLicenses.Shuffle(135643).Append(unallowedLicense).ToArray(); + + string expression = licenses.Length switch + { + 0 => string.Empty, + 1 => licenses[0], + 2 => $"{licenses[0]} AND {licenses[1]}", + _ => licenses.Skip(2).Aggregate($"{licenses[0]} AND {licenses[1]}", (expression, newLicense) => $"{newLicense} AND ({expression})") + }; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression, + new List + { + new ValidationError($"License {expression} not found in list of supported licenses", + _context) + }) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + [Test] [ExtendedAutoData(typeof(NuGetVersionBuilder))] public async Task ValidatingLicensesWithOverwriteLicenseInformation_Should_GiveCorrectResult_If_NotAllowed( @@ -555,6 +691,57 @@ public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_Give .Using(new LicenseValidationResultValueEqualityComparer())); } + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectResult_WithOrExpression_If_OneAllowed( + string packageId, + INuGetVersion packageVersion, + string unallowedLicense) + { + string expression = $"{_allowedLicenses.Shuffle(13563).First()} OR {unallowedLicense}"; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectResult_WithAndExpression_If_AllAllowed( + string packageId, + INuGetVersion packageVersion) + { + string[] licenses = _allowedLicenses.Shuffle(135643).Take(2).ToArray(); + + string expression = $"{licenses[0]} AND {licenses[1]}"; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + [Test] [ExtendedAutoData(typeof(NuGetVersionBuilder))] public async Task ValidatingLicensesWithOverwriteLicenseInformation_Should_GiveCorrectResult_If_Allowed( diff --git a/tests/NuGetUtility.Test/ReferencedPackagesReader/ReferencedPackagesReaderIntegrationTest.cs b/tests/NuGetUtility.Test/ReferencedPackagesReader/ReferencedPackagesReaderIntegrationTest.cs index bd9e21df..0e4be0d3 100644 --- a/tests/NuGetUtility.Test/ReferencedPackagesReader/ReferencedPackagesReaderIntegrationTest.cs +++ b/tests/NuGetUtility.Test/ReferencedPackagesReader/ReferencedPackagesReaderIntegrationTest.cs @@ -62,7 +62,7 @@ public void GetInstalledPackagesShould_ReturnTransitiveNuGet() } [Test] - public void GetInstalledPackagesShould_ReturnEmptyEnumerableForProjectsWithoutPackages() + public void GetInstalledPackagesShould_ReturnEmptyEnumerable_For_ProjectsWithoutPackages() { string path = Path.GetFullPath( "../../../../targets/ProjectWithoutNugetReferences/ProjectWithoutNugetReferences.csproj");