diff --git a/LICENSE b/LICENSE index e052ef62e..aa29e78a0 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,164 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +The binary distribution of Cake.Issues.MsBuild on nuget.org incorporates material from the projects listed below: + +--- + +MSBuild.StructuredLogger + +The MIT License (MIT) + +Copyright (c) 2016 Kirill Osenkov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Microsoft.Build.Framework & Microsoft.Build.Utilities.Core + +MICROSOFT SOFTWARE LICENSE TERMS + +MICROSOFT .NET LIBRARY + +These license terms are an agreement between Microsoft Corporation (or based on where you live, one of its affiliates) and you. They apply to the software named above. The terms also apply to any Microsoft services or updates for the software, except to the extent those have different terms. + +If you comply with these license terms, you have the rights below. +1. INSTALLATION AND USE RIGHTS. + +You may install and use any number of copies of the software to design, develop and test you’re applications. You may modify, copy, distribute or deploy any .js files contained in the software as part of your applications. +2. THIRD PARTY COMPONENTS. The software may include third party components with separate legal notices or governed by other agreements, as may be described in the ThirdPartyNotices file(s) accompanying the software. +3. ADDITIONAL LICENSING REQUIREMENTS AND/OR USE RIGHTS. +a. DISTRIBUTABLE CODE. In addition to the .js files described above, the software is comprised of Distributable Code. “Distributable Code” is code that you are permitted to distribute in programs you develop if you comply with the terms below. +i. Right to Use and Distribute. + +· You may copy and distribute the object code form of the software. + +· Third Party Distribution. You may permit distributors of your programs to copy and distribute the Distributable Code as part of those programs. +ii. Distribution Requirements. For any Distributable Code you distribute, you must + +· use the Distributable Code in your programs and not as a standalone distribution; + +· require distributors and external end users to agree to terms that protect it at least as much as this agreement; + +· display your valid copyright notice on your programs; and + +· indemnify, defend, and hold harmless Microsoft from any claims, including attorneys’ fees, related to the distribution or use of your applications, except to the extent that any claim is based solely on the Distributable Code. +iii. Distribution Restrictions. You may not + +· alter any copyright, trademark or patent notice in the Distributable Code; + +· use Microsoft’s trademarks in your programs’ names or in a way that suggests your programs come from or are endorsed by Microsoft; + +· include Distributable Code in malicious, deceptive or unlawful programs; or + +· modify or distribute the source code of any Distributable Code so that any part of it becomes subject to an Excluded License. An Excluded License is one that requires, as a condition of use, modification or distribution, that + +· the code be disclosed or distributed in source code form; or + +· others have the right to modify it. +4. DATA. +a. Data Collection. The software may collect information about you and your use of the software, and send that to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may opt-out of many of these scenarios, but not all, as described in the product documentation. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft’s privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. +b. Processing of Personal Data. To the extent Microsoft is a processor or subprocessor of personal data in connection with the software, Microsoft makes the commitments in the European Union General Data Protection Regulation Terms of the Online Services Terms to all customers effective May 25, 2018, at http://go.microsoft.com/?linkid=9840733. +5. Scope of License. The software is licensed, not sold. This agreement only gives you some rights to use the software. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the software only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the software that only allow you to use it in certain ways. You may not + +· work around any technical limitations in the software; + +· reverse engineer, decompile or disassemble the software, or otherwise attempt to derive the source code for the software, except and to the extent required by third party licensing terms governing use of certain open source components that may be included in the software; + +· remove, minimize, block or modify any notices of Microsoft or its suppliers in the software; + +· use the software in any way that is against the law; or + +· share, publish, rent or lease the software, provide the software as a stand-alone offering for others to use, or transfer the software or this agreement to any third party. +6. Export Restrictions. You must comply with all domestic and international export laws and regulations that apply to the software, which include restrictions on destinations, end users, and end use. For further information on export restrictions, visit www.microsoft.com/exporting. +7. SUPPORT SERVICES. Because this software is “as is,” we may not provide support services for it. +8. Entire Agreement. This agreement, and the terms for supplements, updates, Internet-based services and support services that you use, are the entire agreement for the software and support services. +9. Applicable Law. If you acquired the software in the United States, Washington law applies to interpretation of and claims for breach of this agreement, and the laws of the state where you live apply to all other claims. If you acquired the software in any other country, its laws apply. +10. CONSUMER RIGHTS; REGIONAL VARIATIONS. This agreement describes certain legal rights. You may have other rights, including consumer rights, under the laws of your state or country. Separate and apart from your relationship with Microsoft, you may also have rights with respect to the party from which you acquired the software. This agreement does not change those other rights if the laws of your state or country do not permit it to do so. For example, if you acquired the software in one of the below regions, or mandatory country law applies, then the following provisions apply to you: +a) Australia. You have statutory guarantees under the Australian Consumer Law and nothing in this agreement is intended to affect those rights. +b) Canada. If you acquired this software in Canada, you may stop receiving updates by turning off the automatic update feature, disconnecting your device from the Internet (if and when you re-connect to the Internet, however, the software will resume checking for and installing updates), or uninstalling the software. The product documentation, if any, may also specify how to turn off updates for your specific device or software. +c) Germany and Austria. + +(i) Warranty. The software will perform substantially as described in any Microsoft materials that accompany it. However, Microsoft gives no contractual guarantee in relation to the software. + +(ii) Limitation of Liability. In case of intentional conduct, gross negligence, claims based on the Product Liability Act, as well as in case of death or personal or physical injury, Microsoft is liable according to the statutory law. +Subject to the foregoing clause (ii), Microsoft will only be liable for slight negligence if Microsoft is in breach of such material contractual obligations, the fulfillment of which facilitate the due performance of this agreement, the breach of which would endanger the purpose of this agreement and the compliance with which a party may constantly trust in (so-called "cardinal obligations"). In other cases of slight negligence, Microsoft will not be liable for slight negligence +11. Disclaimer of Warranty. THE SOFTWARE IS LICENSED “AS-IS.” YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR CONDITIONS. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. +12. Limitation on and Exclusion of Remedies and Damages. YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES. + +This limitation applies to (a) anything related to the software, services, content (including code) on third party Internet sites, or third party applications; and (b) claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law. + +It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your state or country may not allow the exclusion or limitation of incidental, consequential or other damages. + +Please note: As this software is distributed in Quebec, Canada, some of the clauses in this agreement are provided below in French. + + +Remarque : Ce logiciel étant distribué au Québec, Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français. + + + +EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert « tel quel ». Toute utilisation de ce logiciel est à votre seule risque et péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection des consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d’adéquation à un usage particulier et d’absence de contrefaçon sont exclues. + + +LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices. + + +Cette limitation concerne: + +· tout ce qui est relié au logiciel, aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et + +· les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d’une autre faute dans la limite autorisée par la loi en vigueur. + + +Elle s’applique également, même si Microsoft connaissait ou devrait connaître l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l’exclusion ci-dessus ne s’appliquera pas à votre égard. + + +EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas. + +--- + +System.Collections.Immutable + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 2835b0539..db4e6fa11 100644 --- a/README.md +++ b/README.md @@ -112,11 +112,12 @@ For questions and to discuss ideas & feature requests, use the [GitHub discussio ## Addins -| Addin | Description | -|:--:|-| -| [Cake.Issues](https://www.nuget.org/packages/Cake.Issues) | Addin providing the aliases for creating and reading of issues. | -| [Cake.Issues.PullRequests](https://www.nuget.org/packages/Cake.Issues.PullRequests) | Addin providing the aliases for writing issues to pull requests and build servers. | -| [Cake.Issues.Reporting](https://www.nuget.org/packages/Cake.Issues.Reporting) | Addin providing the aliases for creating reports. | +| Cake Scripting Addin | Cake Frosting Addin | Description | +|:--:|-|-| +| [Cake.Issues](https://www.nuget.org/packages/Cake.Issues) | [Cake.Issues](https://www.nuget.org/packages/Cake.Issues) | Addin providing the aliases for creating and reading of issues. | +| [Cake.Issues.PullRequests](https://www.nuget.org/packages/Cake.Issues.PullRequests) | [Cake.Issues.PullRequests](https://www.nuget.org/packages/Cake.Issues.PullRequests) | Addin providing the aliases for writing issues to pull requests and build servers. | +| [Cake.Issues.Reporting](https://www.nuget.org/packages/Cake.Issues.Reporting) | [Cake.Issues.Reporting](https://www.nuget.org/packages/Cake.Issues.Reporting) | Addin providing the aliases for creating reports. | +| [Cake.Issues.MsBuild](https://www.nuget.org/packages/Cake.Issues.MsBuild) | [Cake.FrostingIssues.MsBuild](https://www.nuget.org/packages/Cake.Frosting.Issues.MsBuild) | Issue provider for reading MsBuild errors and warnings. | ## API diff --git a/nuspec/nuget/Cake.Frosting.Issues.MsBuild.nuspec b/nuspec/nuget/Cake.Frosting.Issues.MsBuild.nuspec new file mode 100644 index 000000000..40ee74eee --- /dev/null +++ b/nuspec/nuget/Cake.Frosting.Issues.MsBuild.nuspec @@ -0,0 +1,63 @@ + + + + Cake.Frosting.Issues.MsBuild + Cake.Frosting.Issues.MsBuild + 0.0.0 + BBT Software AG and contributors + bbtsoftware, pascalberger, cake-contrib + MsBuild support for the Cake.Issues addin for Cake Frosting + +The MsBuild support for the Cake.Issues addin for Cake allows you to read issues logged as warnings in a MsBuild log. + +This addin provides the aliases for reading MsBuild warnings and providing them to the Cake.Issues addin. +It also requires the core Cake.Issues addin. + +There are also additional addins for generating reports or posting issues to pull requests. + +See the Project Site for an overview of the whole ecosystem of addins for working with issues in Cake scripts. + + +NOTE: +This is the version of the addin compatible with Cake Frosting. +For addin compatible with Cake Script Runners see Cake.Issues.MsBuild. + + MIT + https://cakeissues.net + icon.png + false + + Copyright © BBT Software AG and contributors + cake cake-addin cake-issues cake-issueprovider code-analysis linting msbuild + https://github.com/cake-contrib/Cake.Issues.MsBuild/releases/tag/4.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nuspec/nuget/Cake.Issues.MsBuild.nuspec b/nuspec/nuget/Cake.Issues.MsBuild.nuspec new file mode 100644 index 000000000..4a119870b --- /dev/null +++ b/nuspec/nuget/Cake.Issues.MsBuild.nuspec @@ -0,0 +1,54 @@ + + + + Cake.Issues.MsBuild + Cake.Issues.MsBuild + 0.0.0 + BBT Software AG and contributors + bbtsoftware, pascalberger, cake-contrib + MsBuild support for the Cake.Issues addin for Cake Build Automation System + +The MsBuild support for the Cake.Issues addin for Cake allows you to read issues logged as warnings in a MsBuild log. + +This addin provides the aliases for reading MsBuild warnings and providing them to the Cake.Issues addin. +It also requires the core Cake.Issues addin. + +There are also additional addins for generating reports or posting issues to pull requests. + +See the Project Site for an overview of the whole ecosystem of addins for working with issues in Cake scripts. + +NOTE: +This is the version of the addin compatible with Cake Script Runners. +For addin compatible with Cake Frosting see Cake.Frosting.Issues.MsBuild. + + MIT + https://cakeissues.net + icon.png + false + + Copyright © BBT Software AG and contributors + cake cake-addin cake-issues cake-issueprovider code-analysis linting msbuild + https://github.com/cake-contrib/Cake.Issues.MsBuild/releases/tag/4.0.0 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/BaseMsBuildLogFileFormatTests.cs b/src/Cake.Issues.MsBuild.Tests/BaseMsBuildLogFileFormatTests.cs new file mode 100644 index 000000000..ad6dfad5c --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/BaseMsBuildLogFileFormatTests.cs @@ -0,0 +1,113 @@ +namespace Cake.Issues.MsBuild.Tests +{ + using Cake.Issues.Testing; + using Cake.Testing; + using Shouldly; + using Xunit; + + public sealed class BaseMsBuildLogFileFormatTests + { + public sealed class TheValidateFilePathMethod + { + [Fact] + public void Should_Throw_If_FilePath_Is_Null() + { + // Given + var format = new FakeMsBuildLogFileFormat(new FakeLog()); + const string filePath = null; + var settings = new RepositorySettings(@"c:\repo"); + + // When + var result = Record.Exception(() => format.ValidateFilePath(filePath, settings)); + + // Then + result.IsArgumentNullException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_Empty() + { + // Given + var format = new FakeMsBuildLogFileFormat(new FakeLog()); + var filePath = string.Empty; + var settings = new RepositorySettings(@"c:\repo"); + + // When + var result = Record.Exception(() => format.ValidateFilePath(filePath, settings)); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_FilePath_Is_WhiteSpace() + { + // Given + var format = new FakeMsBuildLogFileFormat(new FakeLog()); + const string filePath = " "; + var settings = new RepositorySettings(@"c:\repo"); + + // When + var result = Record.Exception(() => format.ValidateFilePath(filePath, settings)); + + // Then + result.IsArgumentOutOfRangeException("filePath"); + } + + [Fact] + public void Should_Throw_If_Settings_Are_Null() + { + // Given + var format = new FakeMsBuildLogFileFormat(new FakeLog()); + const string filePath = @"c:\repo\foo.ch"; + const RepositorySettings settings = null; + + // When + var result = Record.Exception(() => format.ValidateFilePath(filePath, settings)); + + // Then + result.IsArgumentNullException("repositorySettings"); + } + + [Theory] + [InlineData(@"c:\foo\bar.cs", @"c:\foo\", true)] + [InlineData(@"c:\foo\bar.cs", @"c:\foo", true)] + [InlineData(@"c:\foo\bar.cs", @"c:\bar", false)] + public void Should_Return_Correct_Value_For_Valid( + string filePath, + string repoRoot, + bool expectedValue) + { + // Given + var format = new FakeMsBuildLogFileFormat(new FakeLog()); + var settings = new RepositorySettings(repoRoot); + + // When + var (valid, _) = format.ValidateFilePath(filePath, settings); + + // Then + valid.ShouldBe(expectedValue); + } + + [Theory] + [InlineData(@"c:\foo\bar.cs", @"c:\foo\", @"bar.cs")] + [InlineData(@"c:\foo\bar.cs", @"c:\foo", @"bar.cs")] + [InlineData(@"c:\foo\bar.cs", @"c:\bar", @"")] + public void Should_Return_Correct_Value_For_FilePath( + string filePath, + string repoRoot, + string expectedValue) + { + // Given + var format = new FakeMsBuildLogFileFormat(new FakeLog()); + var settings = new RepositorySettings(repoRoot); + + // When + var (_, resultFilePath) = format.ValidateFilePath(filePath, settings); + + // Then + resultFilePath.ShouldBe(expectedValue); + } + } + } +} diff --git a/src/Cake.Issues.MsBuild.Tests/Cake.Issues.MsBuild.Tests.csproj b/src/Cake.Issues.MsBuild.Tests/Cake.Issues.MsBuild.Tests.csproj new file mode 100644 index 000000000..9ca22c6b0 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Cake.Issues.MsBuild.Tests.csproj @@ -0,0 +1,46 @@ + + + + net6.0;net7.0;net8.0 + false + Cake.Issues + Copyright © BBT Software AG and contributors + Tests for the Cake.Issues.MsBuild addin + BBT Software AG + BBT Software AG + + + + ..\Cake.Issues.Tests.ruleset + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/src/Cake.Issues.MsBuild.Tests/FakeMsBuildLogFileFormat.cs b/src/Cake.Issues.MsBuild.Tests/FakeMsBuildLogFileFormat.cs new file mode 100644 index 000000000..a222f3085 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/FakeMsBuildLogFileFormat.cs @@ -0,0 +1,22 @@ +namespace Cake.Issues.MsBuild.Tests +{ + using System; + using System.Collections.Generic; + using Cake.Core.Diagnostics; + + internal class FakeMsBuildLogFileFormat(ICakeLog log) : BaseMsBuildLogFileFormat(log) + { + public new (bool Valid, string FilePath) ValidateFilePath(string filePath, IRepositorySettings repositorySettings) + { + return base.ValidateFilePath(filePath, repositorySettings); + } + + public override IEnumerable ReadIssues( + MsBuildIssuesProvider issueProvider, + IRepositorySettings repositorySettings, + MsBuildIssuesSettings issueProviderSettings) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Cake.Issues.MsBuild.Tests/LogFileFormat/BinaryLogFileFormatTests.cs b/src/Cake.Issues.MsBuild.Tests/LogFileFormat/BinaryLogFileFormatTests.cs new file mode 100644 index 000000000..c81ce634d --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/LogFileFormat/BinaryLogFileFormatTests.cs @@ -0,0 +1,240 @@ +namespace Cake.Issues.MsBuild.Tests.LogFileFormat +{ + using System; + using System.Linq; + using System.Runtime.InteropServices; + using Cake.Core.Diagnostics; + using Cake.Issues.MsBuild.LogFileFormat; + using Cake.Issues.Testing; + using Shouldly; + using Xunit; + + public sealed class BinaryLogFileFormatTests + { + public sealed class TheCtor + { + [Fact] + public void Should_Throw_If_Log_Is_Null() + { + // Given + const ICakeLog log = null; + + // When + var result = Record.Exception(() => new BinaryLogFileFormat(log)); + + // Then + result.IsArgumentNullException("log"); + } + } + + public sealed class TheReadIssuesMethod + { + [SkippableFact] + public void Should_Read_Full_Log_Correct() + { + // Uses Windows specific paths. + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + // Given + var fixture = new MsBuildIssuesProviderFixture("FullLog.binlog") + { + ReadIssuesSettings = new ReadIssuesSettings(@"C:\projects\cake-issues-demo\"), + }; + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(19); + IssueChecker.Check( + issues[0], + IssueBuilder.NewIssue( + "The variable 'foo' is assigned but its value is never used", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 13, 17) + .OfRule("CS0219") + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[1], + IssueBuilder.NewIssue( + "Enable XML documentation output", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 1, 1) + .OfRule("SA1652", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1652.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[2], + IssueBuilder.NewIssue( + "The file header is missing or not located at the top of the file.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 1, 1) + .OfRule("SA1633", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[3], + IssueBuilder.NewIssue( + "Enable XML documentation output", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 1, 1) + .OfRule("SA1652", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1652.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[4], + IssueBuilder.NewIssue( + "The file header is missing or not located at the top of the file.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 1, 1) + .OfRule("SA1633", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[5], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 5, 77) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[6], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 17, 76) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[7], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 18, 74) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[8], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 28, 22) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[9], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 32, 84) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[10], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 1, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[11], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 2, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[12], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 3, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[13], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 4, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[14], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 5, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[15], + IssueBuilder.NewIssue( + "Microsoft.Design : Sign 'ClassLibrary1.dll' with a strong name key.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .OfRule("CA2210", new Uri("https://www.google.com/search?q=\"CA2210:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[16], + IssueBuilder.NewIssue( + "Microsoft.Design : Mark 'ClassLibrary1.dll' with CLSCompliant(true) because it exposes externally visible types.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .OfRule("CA1014", new Uri("https://www.google.com/search?q=\"CA1014:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[17], + IssueBuilder.NewIssue( + "Microsoft.Performance : The 'this' parameter (or 'Me' in Visual Basic) of 'Class1.Foo()' is never used. Mark the member as static (or Shared in Visual Basic) or use 'this'/'Me' in the method body or at least one property accessor, if appropriate.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 12) + .OfRule("CA1822", new Uri("https://www.google.com/search?q=\"CA1822:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[18], + IssueBuilder.NewIssue( + "Microsoft.Performance : 'Class1.Foo()' declares a variable, 'foo', of type 'string', which is never used or is only assigned to. Use this variable or remove it.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 13) + .OfRule("CA1804", new Uri("https://www.google.com/search?q=\"CA1804:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + } + } + } +} diff --git a/src/Cake.Issues.MsBuild.Tests/LogFileFormat/XmlFileLoggerLogFileFormatTests.cs b/src/Cake.Issues.MsBuild.Tests/LogFileFormat/XmlFileLoggerLogFileFormatTests.cs new file mode 100644 index 000000000..2b9d0d5cd --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/LogFileFormat/XmlFileLoggerLogFileFormatTests.cs @@ -0,0 +1,476 @@ +namespace Cake.Issues.MsBuild.Tests.LogFileFormat +{ + using System; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using Cake.Core.Diagnostics; + using Cake.Issues.MsBuild.LogFileFormat; + using Cake.Issues.Testing; + using Shouldly; + using Xunit; + + public sealed class XmlFileLoggerLogFileFormatTests + { + public sealed class TheCtor + { + [Fact] + public void Should_Throw_If_Log_Is_Null() + { + // Given + const ICakeLog log = null; + + // When + var result = Record.Exception(() => new XmlFileLoggerLogFileFormat(log)); + + // Then + result.IsArgumentNullException("log"); + } + } + + public sealed class TheReadIssuesMethod + { + [SkippableFact] + public void Should_Read_Full_Log_Correct() + { + // Uses Windows specific paths. + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + // Given + var fixture = new MsBuildIssuesProviderFixture("FullLog.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(19); + IssueChecker.Check( + issues[0], + IssueBuilder.NewIssue( + "The variable 'foo' is assigned but its value is never used", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 13, 17) + .OfRule("CS0219") + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[1], + IssueBuilder.NewIssue( + "Enable XML documentation output", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 1, 1) + .OfRule("SA1652", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1652.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[2], + IssueBuilder.NewIssue( + "The file header is missing or not located at the top of the file.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 1, 1) + .OfRule("SA1633", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[3], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 1, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[4], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 2, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[5], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 3, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[6], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 4, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[7], + IssueBuilder.NewIssue( + "Using directive must appear within a namespace declaration", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 5, 1) + .OfRule("SA1200", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[8], + IssueBuilder.NewIssue( + "Enable XML documentation output", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 1, 1) + .OfRule("SA1652", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1652.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[9], + IssueBuilder.NewIssue( + "The file header is missing or not located at the top of the file.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 1, 1) + .OfRule("SA1633", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[10], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 5, 77) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[11], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 17, 76) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[12], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 18, 74) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[13], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 28, 22) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[14], + IssueBuilder.NewIssue( + "Code must not contain trailing whitespace", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Properties\AssemblyInfo.cs", 32, 84) + .OfRule("SA1028", new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[15], + IssueBuilder.NewIssue( + "Microsoft.Design : Sign 'ClassLibrary1.dll' with a strong name key.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .OfRule("CA2210", new Uri("https://www.google.com/search?q=\"CA2210:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[16], + IssueBuilder.NewIssue( + "Microsoft.Design : Mark 'ClassLibrary1.dll' with CLSCompliant(true) because it exposes externally visible types.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .OfRule("CA1014", new Uri("https://www.google.com/search?q=\"CA1014:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[17], + IssueBuilder.NewIssue( + "Microsoft.Performance : The 'this' parameter (or 'Me' in Visual Basic) of 'Class1.Foo()' is never used. Mark the member as static (or Shared in Visual Basic) or use 'this'/'Me' in the method body or at least one property accessor, if appropriate.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 12) + .OfRule("CA1822", new Uri("https://www.google.com/search?q=\"CA1822:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[18], + IssueBuilder.NewIssue( + "Microsoft.Performance : 'Class1.Foo()' declares a variable, 'foo', of type 'string', which is never used or is only assigned to. Use this variable or remove it.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject(@"src\ClassLibrary1\ClassLibrary1.csproj", "ClassLibrary1") + .InFile(@"src\ClassLibrary1\Class1.cs", 13) + .OfRule("CA1804", new Uri("https://www.google.com/search?q=\"CA1804:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + } + + [SkippableFact] + public void Should_Read_Issue_With_File_Correct() + { + // Uses Windows specific paths. + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + // Given + var fixture = new MsBuildIssuesProviderFixture("IssueWithFile.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(1); + IssueChecker.Check( + issues.Single(), + IssueBuilder.NewIssue( + @"Microsoft.Usage : 'ConfigurationManager.GetSortedConfigFiles(String)' creates an exception of type 'ApplicationException', an exception type that is not sufficiently specific and should never be raised by user code. If this exception instance might be thrown, use a different exception type.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProjectOfName(string.Empty) + .InFile(@"src\Cake.Issues.MsBuild.Tests\MsBuildIssuesProviderTests.cs", 1311) + .OfRule("CA2201", new Uri("https://www.google.com/search?q=\"CA2201:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + } + + [SkippableFact] + public void Should_Read_Issue_With_File_Without_Path_Correct() + { + // Uses Windows specific paths. + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + // Given + var fixture = new MsBuildIssuesProviderFixture("IssueWithOnlyFileName.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(1); + IssueChecker.Check( + issues.Single(), + IssueBuilder.NewIssue( + "The variable 'foo' is assigned but its value is never used", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProjectOfName(string.Empty) + .InFile(@"src\Cake.Issues.MsBuild.Tests\MsBuildIssuesProviderTests.cs", 13, 17) + .OfRule("CS0219") + .WithPriority(IssuePriority.Warning)); + } + + [SkippableFact] + public void Should_Read_Issue_With_Line_Zero_Correct() + { + // Uses Windows specific paths. + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + // Given + var fixture = new MsBuildIssuesProviderFixture("IssueWithLineZero.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(1); + IssueChecker.Check( + issues.Single(), + IssueBuilder.NewIssue( + @"Unable to locate any documentation sources for 'c:\Source\Cake.Prca\Cake.Prca..csproj' (Configuration: Debug Platform: AnyCPU)", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProject("Cake.Prca.shfbproj", "Cake.Prca") + .InFile("SHFB") + .OfRule("BE0006") + .WithPriority(IssuePriority.Warning)); + } + + [Fact] + public void Should_Read_Issue_Without_File_Correct() + { + // Given + var fixture = new MsBuildIssuesProviderFixture("IssueWithoutFile.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(1); + IssueChecker.Check( + issues.Single(), + IssueBuilder.NewIssue( + "Microsoft.Naming : Rename type name 'UniqueQueue(Of T)' so that it does not end in 'Queue'.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProjectOfName(string.Empty) + .OfRule("CA1711", new Uri("https://www.google.com/search?q=\"CA1711:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + } + + [Fact] + public void Should_Read_Issue_Without_Code_Correct() + { + // Given + var fixture = new MsBuildIssuesProviderFixture("IssueWithoutCode.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(1); + IssueChecker.Check( + issues.Single(), + IssueBuilder.NewIssue( + "SA1300 : CSharp.Naming : namespace names begin with an upper-case letter: foo.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProjectOfName(string.Empty) + .InFile(@"src\Cake.Issues.MsBuild.Tests\MsBuildIssuesProviderTests.cs", 21, 1) + .WithPriority(IssuePriority.Warning)); + } + + [Fact] + public void Should_Ignore_Issue_Without_Message() + { + // Given + var fixture = new MsBuildIssuesProviderFixture("IssueWithoutMessage.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(0); + } + + [Fact] + public void Should_Filter_Control_Chars_From_Log_Content() + { + // Given + var fixture = new MsBuildIssuesProviderFixture("LogWithControlChars.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(0); + } + + [Fact] + public void Should_Read_Issue_With_Absolute_FileName_And_Without_Task() + { + var fixture = new MsBuildIssuesProviderFixture("IssueWithAbsoluteFileNameAndWithoutTask.xml"); + + var repoRootCreated = !Directory.Exists(fixture.ReadIssuesSettings.RepositoryRoot.FullPath); + Directory.CreateDirectory(fixture.ReadIssuesSettings.RepositoryRoot.FullPath); + try + { + var oldWorkingDirectory = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(fixture.ReadIssuesSettings.RepositoryRoot.FullPath); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(1); + } + finally + { + Directory.SetCurrentDirectory(oldWorkingDirectory); + } + } + finally + { + if (repoRootCreated) + { + Directory.Delete(fixture.ReadIssuesSettings.RepositoryRoot.FullPath); + } + } + } + + [Fact] + public void Should_Read_Errors() + { + // Given + var fixture = new MsBuildIssuesProviderFixture("IssueWithError.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(1); + IssueChecker.Check( + issues[0], + IssueBuilder.NewIssue( + @"'ConfigurationManager.GetSortedConfigFiles(String)': not all code paths return a value", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProjectOfName(string.Empty) + .InFile(@"src\Cake.Issues.MsBuild.Tests\MsBuildIssuesProviderTests.cs", 1311) + .OfRule("CS0161") + .WithPriority(IssuePriority.Error)); + } + + [Fact] + public void Should_Read_Both_Warnings_And_Errors() + { + // Given + var fixture = new MsBuildIssuesProviderFixture("IssueWithBothWarningAndErrors.xml"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(2); + IssueChecker.Check( + issues[0], + IssueBuilder.NewIssue( + @"Microsoft.Usage : 'ConfigurationManager.GetSortedConfigFiles(String)' creates an exception of type 'ApplicationException', an exception type that is not sufficiently specific and should never be raised by user code. If this exception instance might be thrown, use a different exception type.", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProjectOfName(string.Empty) + .InFile(@"src\Cake.Issues.MsBuild.Tests\MsBuildIssuesProviderTests.cs", 1311) + .OfRule("CA2201", new Uri("https://www.google.com/search?q=\"CA2201:\"+site:learn.microsoft.com")) + .WithPriority(IssuePriority.Warning)); + IssueChecker.Check( + issues[1], + IssueBuilder.NewIssue( + @"'ConfigurationManager.GetSortedConfigFiles(String)': not all code paths return a value", + "Cake.Issues.MsBuild.MsBuildIssuesProvider", + "MSBuild") + .InProjectOfName(string.Empty) + .InFile(@"src\Cake.Issues.MsBuild.Tests\MsBuildIssuesProviderTests.cs", 1311) + .OfRule("CS0161") + .WithPriority(IssuePriority.Error)); + } + } + } +} diff --git a/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesProviderFixture.cs b/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesProviderFixture.cs new file mode 100644 index 000000000..b3079fe71 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesProviderFixture.cs @@ -0,0 +1,17 @@ +namespace Cake.Issues.MsBuild.Tests +{ + using Cake.Issues.Testing; + + internal class MsBuildIssuesProviderFixture + : BaseMultiFormatIssueProviderFixture + where T : BaseMsBuildLogFileFormat + { + public MsBuildIssuesProviderFixture(string fileResourceName) + : base(fileResourceName) + { + this.ReadIssuesSettings = new ReadIssuesSettings(@"c:\Source\Cake.Issues.MsBuild"); + } + + protected override string FileResourceNamespace => "Cake.Issues.MsBuild.Tests.Testfiles." + typeof(T).Name + "."; + } +} diff --git a/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesProviderTests.cs b/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesProviderTests.cs new file mode 100644 index 000000000..fe45a543b --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesProviderTests.cs @@ -0,0 +1,45 @@ +namespace Cake.Issues.MsBuild.Tests +{ + using Cake.Core.Diagnostics; + using Cake.Issues.MsBuild.LogFileFormat; + using Cake.Issues.Testing; + using Cake.Testing; + using Xunit; + + public sealed class MsBuildIssuesProviderTests + { + public sealed class TheCtor + { + [Fact] + public void Should_Throw_If_Log_Is_Null() + { + // Given + const ICakeLog log = null; + var settings = + new MsBuildIssuesSettings( + "Foo".ToByteArray(), + new XmlFileLoggerLogFileFormat(new FakeLog())); + + // When + var result = Record.Exception(() => new MsBuildIssuesProvider(log, settings)); + + // Then + result.IsArgumentNullException("log"); + } + + [Fact] + public void Should_Throw_If_IssueProviderSettings_Are_Null() + { + // Given + var log = new FakeLog(); + const MsBuildIssuesSettings settings = null; + + // When + var result = Record.Exception(() => new MsBuildIssuesProvider(log, settings)); + + // Then + result.IsArgumentNullException("issueProviderSettings"); + } + } + } +} diff --git a/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesSettingsTests.cs b/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesSettingsTests.cs new file mode 100644 index 000000000..ef31b6fb3 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/MsBuildIssuesSettingsTests.cs @@ -0,0 +1,119 @@ +namespace Cake.Issues.MsBuild.Tests +{ + using System; + using Cake.Core.IO; + using Cake.Issues.MsBuild.LogFileFormat; + using Cake.Issues.Testing; + using Cake.Testing; + using Shouldly; + using Xunit; + + public sealed class MsBuildIssuesSettingsTests + { + public sealed class TheCtor + { + [Fact] + public void Should_Throw_If_LogFilePath_Is_Null() + { + // Given + const FilePath logFilePath = null; + var format = new XmlFileLoggerLogFileFormat(new FakeLog()); + + // When + var result = Record.Exception(() => new MsBuildIssuesSettings(logFilePath, format)); + + // Then + result.IsArgumentNullException("logFilePath"); + } + + [Fact] + public void Should_Throw_If_Format_For_LogFilePath_Is_Null() + { + // Given + const BaseMsBuildLogFileFormat format = null; + + using (var tempFile = new ResourceTempFile("Cake.Issues.MsBuild.Tests.Testfiles.XmlFileLoggerLogFileFormat.FullLog.xml")) + { + // When + var result = Record.Exception(() => + new MsBuildIssuesSettings(tempFile.FileName, format)); + + // Then + result.IsArgumentNullException("format"); + } + } + + [Fact] + public void Should_Throw_If_LogFileContent_Is_Null() + { + // Given + const byte[] logFileContent = null; + var format = new XmlFileLoggerLogFileFormat(new FakeLog()); + + // When + var result = Record.Exception(() => new MsBuildIssuesSettings(logFileContent, format)); + + // Then + result.IsArgumentNullException("logFileContent"); + } + + [Fact] + public void Should_Throw_If_Format_For_LogFileContent_Is_Null() + { + // Given + var logFileContent = "foo".ToByteArray(); + const BaseMsBuildLogFileFormat format = null; + + // When + var result = Record.Exception(() => + new MsBuildIssuesSettings(logFileContent, format)); + + // Then + result.IsArgumentNullException("format"); + } + + [Fact] + public void Should_Set_LogFileContent() + { + // Given + var logFileContent = "Foo".ToByteArray(); + var format = new XmlFileLoggerLogFileFormat(new FakeLog()); + + // When + var settings = new MsBuildIssuesSettings(logFileContent, format); + + // Then + settings.LogFileContent.ShouldBe(logFileContent); + } + + [Fact] + public void Should_Set_LogFileContent_If_Empty() + { + // Given + var logFileContent = Array.Empty(); + var format = new XmlFileLoggerLogFileFormat(new FakeLog()); + + // When + var settings = new MsBuildIssuesSettings(logFileContent, format); + + // Then + settings.LogFileContent.ShouldBe(logFileContent); + } + + [Fact] + public void Should_Set_LogFileContent_From_LogFilePath() + { + // Given + var format = new XmlFileLoggerLogFileFormat(new FakeLog()); + using (var tempFile = new ResourceTempFile("Cake.Issues.MsBuild.Tests.Testfiles.XmlFileLoggerLogFileFormat.FullLog.xml")) + { + // When + var settings = new MsBuildIssuesSettings(tempFile.FileName, format); + + // Then + settings.LogFileContent.ShouldBe(tempFile.Content); + } + } + } + } +} diff --git a/src/Cake.Issues.MsBuild.Tests/MsBuildRuleUrlResolverTests.cs b/src/Cake.Issues.MsBuild.Tests/MsBuildRuleUrlResolverTests.cs new file mode 100644 index 000000000..20019047a --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/MsBuildRuleUrlResolverTests.cs @@ -0,0 +1,98 @@ +namespace Cake.Issues.MsBuild.Tests +{ + using System; + using Cake.Issues.Testing; + using Shouldly; + using Xunit; + + public sealed class MsBuildRuleUrlResolverTests + { + public sealed class TheResolveRuleUrlMethod + { + [Fact] + public void Should_Throw_If_Rule_Is_Null() + { + // Given / When + var result = Record.Exception(() => MsBuildRuleUrlResolver.Instance.ResolveRuleUrl(null)); + + // Then + result.IsArgumentNullException("rule"); + } + + [Fact] + public void Should_Throw_If_Rule_Is_Empty() + { + // Given / When + var result = Record.Exception(() => MsBuildRuleUrlResolver.Instance.ResolveRuleUrl(string.Empty)); + + // Then + result.IsArgumentOutOfRangeException("rule"); + } + + [Fact] + public void Should_Throw_If_Rule_Is_WhiteSpace() + { + // Given / When + var result = Record.Exception(() => MsBuildRuleUrlResolver.Instance.ResolveRuleUrl(" ")); + + // Then + result.IsArgumentOutOfRangeException("rule"); + } + + [Theory] + [InlineData("CA2201", "https://www.google.com/search?q=\"CA2201:\"+site:learn.microsoft.com")] + [InlineData("SA1652", "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1652.md")] + [InlineData("S1075", "https://rules.sonarsource.com/csharp/RSPEC-1075")] + [InlineData("RCS1001", "https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1001.md")] + public void Should_Resolve_Url(string rule, string expectedUrl) + { + // Given + var urlResolver = MsBuildRuleUrlResolver.Instance; + + // When + var ruleUrl = urlResolver.ResolveRuleUrl(rule); + + // Then + ruleUrl.ToString().ShouldBe(expectedUrl); + } + + [Theory] + [InlineData("CA")] + [InlineData("2201")] + [InlineData("CA2201Foo")] + [InlineData("CS0219")] + public void Should_Return_Null_For_Unknown_Rules(string rule) + { + // Given + var urlResolver = MsBuildRuleUrlResolver.Instance; + + // When + var ruleUrl = urlResolver.ResolveRuleUrl(rule); + + // Then + ruleUrl.ShouldBeNull(); + } + + [Fact] + public void Should_Resolve_Url_From_Custom_Resolvers() + { + // Given + const string foo = "FOO123"; + const string fooUrl = "http://foo.com/"; + const string bar = "BAR123"; + const string barUrl = "http://bar.com/"; + var urlResolver = MsBuildRuleUrlResolver.Instance; + urlResolver.AddUrlResolver(x => x.Rule == foo ? new Uri(fooUrl) : null); + urlResolver.AddUrlResolver(x => x.Rule == bar ? new Uri(barUrl) : null); + + // When + var fooRuleUrl = urlResolver.ResolveRuleUrl(foo); + var barRuleUrl = urlResolver.ResolveRuleUrl(bar); + + // Then + fooRuleUrl.ToString().ShouldBe(fooUrl); + barRuleUrl.ToString().ShouldBe(barUrl); + } + } + } +} diff --git a/src/Cake.Issues.MsBuild.Tests/Properties/ProjectInfo.cs b/src/Cake.Issues.MsBuild.Tests/Properties/ProjectInfo.cs new file mode 100644 index 000000000..72ce1114f --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Properties/ProjectInfo.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c69329f6-a4d1-4e33-bd9d-5e59973be781")] \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/BinaryLogFileFormat/FullLog.binlog b/src/Cake.Issues.MsBuild.Tests/Testfiles/BinaryLogFileFormat/FullLog.binlog new file mode 100644 index 000000000..d8ae2cb46 Binary files /dev/null and b/src/Cake.Issues.MsBuild.Tests/Testfiles/BinaryLogFileFormat/FullLog.binlog differ diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/FullLog.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/FullLog.xml new file mode 100644 index 000000000..a4af77e5b --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/FullLog.xml @@ -0,0 +1,925 @@ + + + + + + + Debug|AnyCPU +' == '') and ('' != 'true')).]]> + + Debug|AnyCPU +' == '') and ('' == 'true')).]]> + + + + + + + + + + + + + + + + Debug|AnyCPU +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Count())' != '0' and '$(ReferringTargetFrameworkForProjectReferences)' != '') was evaluated as ('0' != '0' and '.NETFramework,Version=v4.5.2' != '').]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + '%(WithCulture)') != '') was evaluated as ( != '').]]> + + + + + + '%(WithCulture)') != '') was evaluated as ( != '').]]> + + + + + + + + + + + + + + + + + + + + + + + + + + C:\Source\Cake.Issues.MsBuild\src\ClassLibrary1\bin\Debug\ClassLibrary1.dll]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 19 + 0 + 30.05.2018 12:16:00 + 30.05.2018 12:16:09 + 00:00:09.5536676 + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithAbsoluteFileNameAndWithoutTask.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithAbsoluteFileNameAndWithoutTask.xml new file mode 100644 index 000000000..158a97a24 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithAbsoluteFileNameAndWithoutTask.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithBothWarningAndErrors.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithBothWarningAndErrors.xml new file mode 100644 index 000000000..06ae93382 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithBothWarningAndErrors.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithError.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithError.xml new file mode 100644 index 000000000..610acdc0e --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithError.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithFile.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithFile.xml new file mode 100644 index 000000000..f4cc1739c --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithFile.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithLineZero.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithLineZero.xml new file mode 100644 index 000000000..b021f47f8 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithLineZero.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithOnlyFileName.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithOnlyFileName.xml new file mode 100644 index 000000000..965aea2e0 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithOnlyFileName.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutCode.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutCode.xml new file mode 100644 index 000000000..424fa2c65 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutCode.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutFile.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutFile.xml new file mode 100644 index 000000000..fd20b4d52 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutFile.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutMessage.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutMessage.xml new file mode 100644 index 000000000..94088c117 --- /dev/null +++ b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/IssueWithoutMessage.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/LogWithControlChars.xml b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/LogWithControlChars.xml new file mode 100644 index 000000000..619b45f85 Binary files /dev/null and b/src/Cake.Issues.MsBuild.Tests/Testfiles/XmlFileLoggerLogFileFormat/LogWithControlChars.xml differ diff --git a/src/Cake.Issues.MsBuild/BaseMsBuildLogFileFormat.cs b/src/Cake.Issues.MsBuild/BaseMsBuildLogFileFormat.cs new file mode 100644 index 000000000..4fda4c1ff --- /dev/null +++ b/src/Cake.Issues.MsBuild/BaseMsBuildLogFileFormat.cs @@ -0,0 +1,42 @@ +namespace Cake.Issues.MsBuild +{ + using Cake.Core.Diagnostics; + + /// + /// Base class for all MSBuild log file format. + /// + public abstract class BaseMsBuildLogFileFormat : BaseLogFileFormat + { + /// + /// Initializes a new instance of the class. + /// + /// The Cake log instance. + protected BaseMsBuildLogFileFormat(ICakeLog log) + : base(log) + { + } + + /// + /// Validates a file path. + /// + /// Full file path. + /// Repository settings. + /// Tuple containing a value if validation was successful, and file path relative to repository root. + protected (bool Valid, string FilePath) ValidateFilePath(string filePath, IRepositorySettings repositorySettings) + { + filePath.NotNullOrWhiteSpace(nameof(filePath)); + repositorySettings.NotNull(nameof(repositorySettings)); + + // Ignore files from outside the repository. + if (!filePath.IsInRepository(repositorySettings)) + { + return (false, string.Empty); + } + + // Make path relative to repository root. + filePath = filePath.MakeFilePathRelativeToRepositoryRoot(repositorySettings); + + return (true, filePath); + } + } +} diff --git a/src/Cake.Issues.MsBuild/Cake.Issues.MsBuild.csproj b/src/Cake.Issues.MsBuild/Cake.Issues.MsBuild.csproj new file mode 100644 index 000000000..3c9e655bf --- /dev/null +++ b/src/Cake.Issues.MsBuild/Cake.Issues.MsBuild.csproj @@ -0,0 +1,52 @@ + + + + net6.0;net7.0;net8.0 + MsBuild support for the Cake.Issues Addin for Cake Build Automation System + BBT Software AG + BBT Software AG + Copyright © BBT Software AG and contributors + Cake.Issues + + + + bin\$(Configuration)\$(TargetFramework)\Cake.Issues.MsBuild.xml + full + true + AllEnabledByDefault + ..\Cake.Issues.ruleset + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + diff --git a/src/Cake.Issues.MsBuild/LogFileFormat/BinaryLogFileFormat.cs b/src/Cake.Issues.MsBuild/LogFileFormat/BinaryLogFileFormat.cs new file mode 100644 index 000000000..50e56ec68 --- /dev/null +++ b/src/Cake.Issues.MsBuild/LogFileFormat/BinaryLogFileFormat.cs @@ -0,0 +1,253 @@ +namespace Cake.Issues.MsBuild.LogFileFormat +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Cake.Core.Diagnostics; + using Microsoft.Build.Framework; + using Microsoft.Build.Logging.StructuredLogger; + + /// + /// MsBuild binary log file format. + /// + /// The Cake log instance. + internal class BinaryLogFileFormat(ICakeLog log) : BaseMsBuildLogFileFormat(log) + { + /// + public override IEnumerable ReadIssues( + MsBuildIssuesProvider issueProvider, + IRepositorySettings repositorySettings, + MsBuildIssuesSettings issueProviderSettings) + { + issueProvider.NotNull(nameof(issueProvider)); + repositorySettings.NotNull(nameof(repositorySettings)); + issueProviderSettings.NotNull(nameof(issueProviderSettings)); + + var result = new List(); + + var binLogReader = new BinLogReader(); + foreach (var buildEventArgs in binLogReader.ReadRecords(issueProviderSettings.LogFileContent).Select(x => x.Args)) + { + IIssue issue = null; + if (buildEventArgs is BuildErrorEventArgs buildError) + { + issue = this.GetIssue(buildError, issueProvider, repositorySettings); + } + else if (buildEventArgs is BuildWarningEventArgs buildWarning) + { + issue = this.GetIssue(buildWarning, issueProvider, repositorySettings); + } + + if (issue == null) + { + continue; + } + + result.Add(issue); + } + + return result; + } + + /// + /// Returns the column based on the value from a MsBuild log. + /// + /// Raw value from MsBuild log. + /// Column number or null if warning or error is not related to a file. + private static int? GetColumn(int column) + { + // Convert negative column numbers or column number 0 to null + if (column <= 0) + { + return null; + } + + return column; + } + + /// + /// Returns the line based on the value from a MsBuild log. + /// + /// Raw value from MsBuild log. + /// Line number or null if warning or error is not related to a file. + private static int? GetLine(int line) + { + // Convert negative line numbers or line number 0 to null + if (line <= 0) + { + return null; + } + + return line; + } + + /// + /// Returns an issue for a build error. + /// + /// Error for which the issue should be returned. + /// Issue provider instance. + /// Repository settings to use. + /// Issue instance or null, if the could not be parsed. + private IIssue GetIssue( + BuildErrorEventArgs buildError, + MsBuildIssuesProvider issueProvider, + IRepositorySettings repositorySettings) + { + this.Log.Verbose("Process error '{0}'...", buildError.Message); + + return + this.GetIssue( + IssuePriority.Error, + buildError.Message, + buildError.ProjectFile, + buildError.File, + buildError.LineNumber, + buildError.EndLineNumber, + buildError.ColumnNumber, + buildError.EndColumnNumber, + buildError.Code, + issueProvider, + repositorySettings); + } + + /// + /// Returns an issue for a build warning. + /// + /// Warning for which the issue should be returned. + /// Issue provider instance. + /// Repository settings to use. + /// Issue instance or null, if the could not be parsed. + private IIssue GetIssue( + BuildWarningEventArgs buildWarning, + MsBuildIssuesProvider issueProvider, + IRepositorySettings repositorySettings) + { + this.Log.Verbose("Process warning '{0}'...", buildWarning.Message); + + return + this.GetIssue( + IssuePriority.Warning, + buildWarning.Message, + buildWarning.ProjectFile, + buildWarning.File, + buildWarning.LineNumber, + buildWarning.EndLineNumber, + buildWarning.ColumnNumber, + buildWarning.EndColumnNumber, + buildWarning.Code, + issueProvider, + repositorySettings); + } + + /// + /// Returns an issue for values from an MsBuild log. + /// + /// Priority of the issue. + /// Raw value from the MsBuild log containing the message. + /// Raw value from the MsBuild log containing the project file. + /// Raw value from the MsBuild log containing the file. + /// Raw value from the MsBuild log containing the line number. + /// Raw value from the MsBuild log containing the end of the line range. + /// Raw value from the MsBuild log containing the column. + /// Raw value from the MsBuild log containing the end of the column range. + /// Raw value from the MsBuild log containing the rule. + /// Issue provider instance. + /// Repository settings to use. + /// Issue instance or null, if the values could not be parsed. + private IIssue GetIssue( + IssuePriority priority, + string message, + string projectFile, + string file, + int lineNumber, + int endLineNumber, + int columnNumber, + int endColumnNumber, + string code, + MsBuildIssuesProvider issueProvider, + IRepositorySettings repositorySettings) + { + // Ignore warnings or errors without a message. + if (string.IsNullOrWhiteSpace(message)) + { + this.Log.Verbose("Skip element since it doesn't contain a message"); + return null; + } + + var projectFileRelativePath = this.GetProject(projectFile, repositorySettings); + + // Read affected file from the warning or error. + var (result, fileName) = this.TryGetFile(file, projectFile, repositorySettings); + if (!result) + { + this.Log.Information("Skip element since file path could not be parsed"); + return null; + } + + var line = GetLine(lineNumber); + var endLine = GetLine(endLineNumber); + var column = GetColumn(columnNumber); + var endColumn = GetColumn(endColumnNumber); + var rule = code; + + // Determine rule URL. + Uri ruleUrl = null; + if (!string.IsNullOrWhiteSpace(rule)) + { + ruleUrl = MsBuildRuleUrlResolver.Instance.ResolveRuleUrl(rule); + } + + // Build issue. + return + IssueBuilder + .NewIssue(message, issueProvider) + .WithPriority(priority) + .InProject(projectFileRelativePath, System.IO.Path.GetFileNameWithoutExtension(projectFileRelativePath)) + .InFile(fileName, line, endLine, column, endColumn) + .OfRule(rule, ruleUrl) + .Create(); + } + + /// + /// Determines the project from a value in a MsBuild log. + /// + /// Raw value from the MsBuild log. + /// Repository settings to use. + /// Relative path to the project. + private string GetProject( + string project, + IRepositorySettings repositorySettings) + { + // Validate project path and make relative to repository root. + return this.ValidateFilePath(project, repositorySettings).FilePath; + } + + /// + /// Reads the affected file path from a value in a MsBuild log. + /// + /// Raw value for file path from MsBuild log. + /// Raw value for project path from the MsBuild log. + /// Repository settings to use. + /// True if the file path could be parsed and the full path to the affected file. + private (bool successful, string fileName) TryGetFile( + string fileName, + string project, + IRepositorySettings repositorySettings) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + return (true, fileName); + } + + // If not absolute path, combine with file path from project file. + if (!fileName.IsFullPath()) + { + var projectDirectory = System.IO.Path.GetDirectoryName(project); + fileName = System.IO.Path.Combine(projectDirectory, fileName); + } + + // Validate file path and make relative to repository root. + return this.ValidateFilePath(fileName, repositorySettings); + } + } +} diff --git a/src/Cake.Issues.MsBuild/LogFileFormat/XmlFileLoggerLogFileFormat.cs b/src/Cake.Issues.MsBuild/LogFileFormat/XmlFileLoggerLogFileFormat.cs new file mode 100644 index 000000000..45bad4114 --- /dev/null +++ b/src/Cake.Issues.MsBuild/LogFileFormat/XmlFileLoggerLogFileFormat.cs @@ -0,0 +1,274 @@ +namespace Cake.Issues.MsBuild.LogFileFormat +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Xml.Linq; + using Cake.Core.Diagnostics; + + /// + /// MsBuild log format as written by the XmlFileLogger class from MSBuild Extension Pack. + /// + /// The Cake log instance. + internal class XmlFileLoggerLogFileFormat(ICakeLog log) : BaseMsBuildLogFileFormat(log) + { + /// + public override IEnumerable ReadIssues( + MsBuildIssuesProvider issueProvider, + IRepositorySettings repositorySettings, + MsBuildIssuesSettings issueProviderSettings) + { + issueProvider.NotNull(nameof(issueProvider)); + repositorySettings.NotNull(nameof(repositorySettings)); + issueProviderSettings.NotNull(nameof(issueProviderSettings)); + + var result = new List(); + + // Read log file. + var raw = issueProviderSettings.LogFileContent.ToStringUsingEncoding(true); + var filtered = string.Concat(raw.Where(c => !char.IsControl(c))); + var logDocument = XDocument.Parse(filtered); + + // Loop through all warning and error tags. + var elements = new List(logDocument.Descendants("warning")); + elements.AddRange(logDocument.Descendants("error")); + + foreach (var element in elements) + { + this.Log.Verbose("Process element '{0}'...", element); + + // Ignore warnings or errors without a message. + if (string.IsNullOrWhiteSpace(element.Value)) + { + this.Log.Verbose("Skip element since it doesn't contain a message"); + continue; + } + + // Read affected project from the warning or error. + if (!this.TryGetProject(element, repositorySettings, out var projectFileRelativePath)) + { + this.Log.Information("Skip element since project could not be parsed"); + continue; + } + + // Read affected file from the warning or error. + if (!this.TryGetFile(element, repositorySettings, out var fileName)) + { + this.Log.Information("Skip element since file path could not be parsed"); + continue; + } + + // Read affected line from the warning or error. + if (!this.TryGetLine(element, out var line)) + { + this.Log.Information("Skip element since line could not be parsed"); + continue; + } + + // Read affected column from the warning or error. + if (!this.TryGetColumn(element, out var column)) + { + this.Log.Information("Skip element since column could not be parsed"); + continue; + } + + // Read rule code from the warning or error. + if (!this.TryGetRule(element, out var rule)) + { + this.Log.Information("Skip element since rule could not be parsed"); + continue; + } + + // Determine rule URL. + Uri ruleUrl = null; + if (!string.IsNullOrWhiteSpace(rule)) + { + ruleUrl = MsBuildRuleUrlResolver.Instance.ResolveRuleUrl(rule); + } + + var priority = element.Name.LocalName == "error" ? IssuePriority.Error : IssuePriority.Warning; + + // Build issue. + result.Add( + IssueBuilder + .NewIssue(element.Value, issueProvider) + .WithPriority(priority) + .InProject(projectFileRelativePath, System.IO.Path.GetFileNameWithoutExtension(projectFileRelativePath)) + .InFile(fileName, line, column) + .OfRule(rule, ruleUrl) + .Create()); + } + + return result; + } + + /// + /// Determines the project for a warning or error logged in a MsBuild log. + /// + /// Warning or error element from MsBuild log. + /// Repository settings to use. + /// Returns project. + /// True if the project could be parsed. + private bool TryGetProject( + XElement element, + IRepositorySettings repositorySettings, + out string project) + { + project = string.Empty; + + var projectNode = element.Ancestors("project").FirstOrDefault(); + if (projectNode == null) + { + this.Log.Information("Project not found for element '{0}'", element); + return true; + } + + var projectAttr = projectNode.Attribute("file"); + if (projectAttr == null) + { + this.Log.Information("File not found for element '{0}'", element); + return true; + } + + project = projectAttr.Value; + if (string.IsNullOrWhiteSpace(project)) + { + this.Log.Information("Project path not found for element '{0}'", element); + return true; + } + + // Validate project path and make relative to repository root. + (var result, project) = this.ValidateFilePath(project, repositorySettings); + return result; + } + + /// + /// Reads the affected file path from a warning or error logged in a MsBuild log. + /// + /// Warning or error element from MsBuild log. + /// Repository settings to use. + /// Returns the full path to the affected file. + /// True if the file path could be parsed. + private bool TryGetFile( + XElement element, + IRepositorySettings repositorySettings, + out string fileName) + { + fileName = string.Empty; + + var fileAttr = element.Attribute("file"); + if (fileAttr == null) + { + this.Log.Verbose("File attribute not found for element '{0}'", element); + return true; + } + + fileName = fileAttr.Value; + if (string.IsNullOrWhiteSpace(fileName)) + { + this.Log.Information("File path not found for element '{0}'", element); + return true; + } + + // If not absolute path, combine with file path from compile task. + if (!fileName.IsFullPath()) + { + var parentFileAttr = element.Parent?.Attribute("file"); + if (parentFileAttr != null) + { + var compileTaskDirectory = System.IO.Path.GetDirectoryName(parentFileAttr.Value); + fileName = System.IO.Path.Combine(compileTaskDirectory, fileName); + } + else + { + fileName = System.IO.Path.Combine(repositorySettings.RepositoryRoot.FullPath, fileName); + } + } + + // Validate file path and make relative to repository root. + (var result, fileName) = this.ValidateFilePath(fileName, repositorySettings); + return result; + } + + /// + /// Reads the affected line from a warning or error logged in a MsBuild log. + /// + /// Warning or error element from MsBuild log. + /// Returns line. + /// True if the line could be parsed. + private bool TryGetLine(XElement element, out int? line) + { + line = null; + + var lineAttr = element.Attribute("line"); + + var lineValue = lineAttr?.Value; + if (string.IsNullOrWhiteSpace(lineValue)) + { + return false; + } + + line = int.Parse(lineValue, CultureInfo.InvariantCulture); + + // Convert negative line numbers or line number 0 to null + if (line <= 0) + { + this.Log.Information("Ignore value {0} since it is outside of the allowed range for line property.", line); + line = null; + } + + return true; + } + + /// + /// Reads the affected column from a warning or error logged in a MsBuild log. + /// + /// Warning or error element from MsBuild log. + /// Returns column. + /// True if the column could be parsed. + private bool TryGetColumn(XElement element, out int? column) + { + column = null; + + var columnAttr = element.Attribute("column"); + + var columnValue = columnAttr?.Value; + if (string.IsNullOrWhiteSpace(columnValue)) + { + return false; + } + + column = int.Parse(columnValue, CultureInfo.InvariantCulture); + + // Convert negative column numbers or column number 0 to null + if (column <= 0) + { + this.Log.Information("Ignore value {0} since it is outside of the allowed range for column property.", column); + column = null; + } + + return true; + } + + /// + /// Reads the rule code from a warning or error logged in a MsBuild log. + /// + /// Warning or error element from MsBuild log. + /// Returns the code of the rule. + /// True if the rule code could be parsed. + private bool TryGetRule(XElement element, out string rule) + { + var codeAttr = element.Attribute("code"); + if (codeAttr == null) + { + this.Log.Verbose("code attribute not found for element '{0}'", element); + rule = null; + return true; + } + + rule = codeAttr.Value; + return !string.IsNullOrWhiteSpace(rule); + } + } +} diff --git a/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.BinaryLogFileFormat.cs b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.BinaryLogFileFormat.cs new file mode 100644 index 000000000..13b3568c3 --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.BinaryLogFileFormat.cs @@ -0,0 +1,27 @@ +namespace Cake.Issues.MsBuild +{ + using Cake.Core; + using Cake.Core.Annotations; + using Cake.Issues.MsBuild.LogFileFormat; + + /// + /// Contains functionality related to . + /// + public static partial class MsBuildIssuesAliases + { + /// + /// Gets an instance for the MsBuild binary log format. + /// + /// The context. + /// Instance for the MsBuild binary log format. + [CakePropertyAlias] + [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)] + public static BaseMsBuildLogFileFormat MsBuildBinaryLogFileFormat( + this ICakeContext context) + { + context.NotNull(nameof(context)); + + return new BinaryLogFileFormat(context.Log); + } + } +} diff --git a/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.IssueProvider.cs b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.IssueProvider.cs new file mode 100644 index 000000000..4209fc41b --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.IssueProvider.cs @@ -0,0 +1,132 @@ +namespace Cake.Issues.MsBuild +{ + using Cake.Core; + using Cake.Core.Annotations; + using Cake.Core.IO; + + /// + /// Contains functionality related to . + /// + public static partial class MsBuildIssuesAliases + { + /// + /// Gets the name of the MsBuild issue provider. + /// This name can be used to identify issues based on the property. + /// + /// The context. + /// Name of the MsBuild issue provider. + [CakePropertyAlias] + [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)] + public static string MsBuildIssuesProviderTypeName( + this ICakeContext context) + { + context.NotNull(nameof(context)); + + return MsBuildIssuesProvider.ProviderTypeName; + } + + /// + /// Gets an instance of a provider for issues reported as MsBuild warnings using a log file from disk. + /// + /// The context. + /// Path to the the MsBuild log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided MsBuild log file. + /// Instance of a provider for issues reported as MsBuild warnings. + /// + /// Read issues reported as MsBuild warnings: + /// + /// + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)] + public static IIssueProvider MsBuildIssuesFromFilePath( + this ICakeContext context, + FilePath logFilePath, + BaseMsBuildLogFileFormat format) + { + context.NotNull(nameof(context)); + logFilePath.NotNull(nameof(logFilePath)); + format.NotNull(nameof(format)); + + return context.MsBuildIssues(new MsBuildIssuesSettings(logFilePath, format)); + } + + /// + /// Gets an instance of a provider for issues reported as MsBuild warnings using log content. + /// + /// The context. + /// Content of the the MsBuild log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided MsBuild log file. + /// Instance of a provider for issues reported as MsBuild warnings. + /// + /// Read issues reported as MsBuild warnings: + /// + /// + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)] + public static IIssueProvider MsBuildIssuesFromContent( + this ICakeContext context, + string logFileContent, + BaseMsBuildLogFileFormat format) + { + context.NotNull(nameof(context)); + logFileContent.NotNullOrWhiteSpace(nameof(logFileContent)); + format.NotNull(nameof(format)); + + return context.MsBuildIssues(new MsBuildIssuesSettings(logFileContent.ToByteArray(), format)); + } + + /// + /// Gets an instance of a provider for issues reported as MsBuild warnings using specified settings. + /// + /// The context. + /// Settings for reading the MSBuild log. + /// Instance of a provider for issues reported as MsBuild warnings. + /// + /// Read issues reported as MsBuild warnings: + /// + /// + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)] + public static IIssueProvider MsBuildIssues( + this ICakeContext context, + MsBuildIssuesSettings settings) + { + context.NotNull(nameof(context)); + settings.NotNull(nameof(settings)); + + return new MsBuildIssuesProvider(context.Log, settings); + } + } +} diff --git a/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.RuleUrlResolver.cs b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.RuleUrlResolver.cs new file mode 100644 index 000000000..ce72a20f3 --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.RuleUrlResolver.cs @@ -0,0 +1,76 @@ +namespace Cake.Issues.MsBuild +{ + using System; + using Cake.Core; + using Cake.Core.Annotations; + + /// + /// Contains functionality related to rule url resolving. + /// + public static partial class MsBuildIssuesAliases + { + /// + /// Registers a new URL resolver with default priority of 0. + /// + /// The context. + /// Resolver which returns an linking to a site + /// containing help for a specific . + /// + /// Adds a provider with default priority of 0 returning a link for all rules of the category CA to + /// search msdn.microsoft.com with Google for the rule: + /// + /// + /// x.Category.ToUpperInvariant() == "CA" ? + /// new Uri("https://www.google.com/search?q=%22" + x.Rule + ":%22+site:msdn.microsoft.com") : + /// null) + /// ]]> + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)] + public static void MsBuildAddRuleUrlResolver( + this ICakeContext context, + Func resolver) + { + context.NotNull(nameof(context)); + resolver.NotNull(nameof(resolver)); + + MsBuildRuleUrlResolver.Instance.AddUrlResolver(resolver); + } + + /// + /// Registers a new URL resolver with a specific priority. + /// + /// The context. + /// Resolver which returns an linking to a site + /// containing help for a specific . + /// Priority of the resolver. Resolver with a higher priority are considered + /// first during resolving of the URL. + /// + /// Adds a provider of priority 5 returning a link for all rules of the category CA to + /// search msdn.microsoft.com with Google for the rule: + /// + /// + /// x.Category.ToUpperInvariant() == "CA" ? + /// new Uri("https://www.google.com/search?q=%22" + x.Rule + ":%22+site:msdn.microsoft.com") : + /// null, + /// 5) + /// ]]> + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)] + public static void MsBuildAddRuleUrlResolver( + this ICakeContext context, + Func resolver, + int priority) + { + context.NotNull(nameof(context)); + resolver.NotNull(nameof(resolver)); + + MsBuildRuleUrlResolver.Instance.AddUrlResolver(resolver, priority); + } + } +} diff --git a/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.XmlFileLoggerLogFileFormat.cs b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.XmlFileLoggerLogFileFormat.cs new file mode 100644 index 000000000..dd63844a7 --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.XmlFileLoggerLogFileFormat.cs @@ -0,0 +1,50 @@ +namespace Cake.Issues.MsBuild +{ + using Cake.Core; + using Cake.Core.Annotations; + using Cake.Issues.MsBuild.LogFileFormat; + + /// + /// Contains functionality related to . + /// + public static partial class MsBuildIssuesAliases + { + /// + /// + /// Gets an instance for the MsBuild log format as written by the XmlFileLogger class + /// from MSBuild Extension Pack. + /// + /// + /// You can add the logger to the MSBuildSettings like this: + /// + /// var settings = new MSBuildSettings() + /// .WithLogger( + /// Context.Tools.Resolve("MSBuild.ExtensionPack.Loggers.dll").FullPath, + /// "XmlFileLogger", + /// string.Format( + /// "logfile=\"{0}\";verbosity=Detailed;encoding=UTF-8", + /// @"c:\build\msbuild.log") + /// ); + /// + /// + /// + /// In order to use the above logger, include the following in your build.cake file to download and + /// install from NuGet.org: + /// + /// #tool "nuget:?package=MSBuild.Extension.Pack" + /// + /// + /// + /// The context. + /// Instance for the MsBuild log format. + [CakePropertyAlias] + [CakeAliasCategory(IssuesAliasConstants.IssueProviderCakeAliasCategory)] + public static BaseMsBuildLogFileFormat MsBuildXmlFileLoggerFormat( + this ICakeContext context) + { + context.NotNull(nameof(context)); + + return new XmlFileLoggerLogFileFormat(context.Log); + } + } +} diff --git a/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.cs b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.cs new file mode 100644 index 000000000..76a5af225 --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildIssuesAliases.cs @@ -0,0 +1,15 @@ +namespace Cake.Issues.MsBuild +{ + using Cake.Core.Annotations; + + /// + /// Contains functionality for reading warnings from MSBuild log files. + /// + /// NOTE: Use Cake.Issues.MsBuild addin to use these aliases with Cake Script Runners and + /// Cake.Frosting.Issues.MsBuild to use these aliases with Cake Frosting. + /// + [CakeAliasCategory(IssuesAliasConstants.MainCakeAliasCategory)] + public static partial class MsBuildIssuesAliases + { + } +} diff --git a/src/Cake.Issues.MsBuild/MsBuildIssuesProvider.cs b/src/Cake.Issues.MsBuild/MsBuildIssuesProvider.cs new file mode 100644 index 000000000..7dca2fabb --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildIssuesProvider.cs @@ -0,0 +1,21 @@ +namespace Cake.Issues.MsBuild +{ + using Cake.Core.Diagnostics; + + /// + /// Provider for issues reported as MsBuild warnings. + /// + /// The Cake log context. + /// Settings for reading the log file. + public class MsBuildIssuesProvider(ICakeLog log, MsBuildIssuesSettings settings) : BaseMultiFormatIssueProvider(log, settings) + { + /// + /// Gets the name of the MsBuild issue provider. + /// This name can be used to identify issues based on the property. + /// + public static string ProviderTypeName => typeof(MsBuildIssuesProvider).FullName; + + /// + public override string ProviderName => "MSBuild"; + } +} diff --git a/src/Cake.Issues.MsBuild/MsBuildIssuesSettings.cs b/src/Cake.Issues.MsBuild/MsBuildIssuesSettings.cs new file mode 100644 index 000000000..8405c9117 --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildIssuesSettings.cs @@ -0,0 +1,34 @@ +namespace Cake.Issues.MsBuild +{ + using Cake.Core.IO; + + /// + /// Settings for . + /// + public class MsBuildIssuesSettings : BaseMultiFormatIssueProviderSettings + { + /// + /// Initializes a new instance of the class + /// for reading a log file on disk. + /// + /// Path to the MSBuild log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided MSBuild log file. + public MsBuildIssuesSettings(FilePath logFilePath, BaseMsBuildLogFileFormat format) + : base(logFilePath, format) + { + } + + /// + /// Initializes a new instance of the class + /// for a log file content in memory. + /// + /// Content of the MSBuild log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided MSBuild log file. + public MsBuildIssuesSettings(byte[] logFileContent, BaseMsBuildLogFileFormat format) + : base(logFileContent, format) + { + } + } +} diff --git a/src/Cake.Issues.MsBuild/MsBuildRuleDescription.cs b/src/Cake.Issues.MsBuild/MsBuildRuleDescription.cs new file mode 100644 index 000000000..33917edab --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildRuleDescription.cs @@ -0,0 +1,18 @@ +namespace Cake.Issues.MsBuild +{ + /// + /// Class describing rules appearing in MsBuild logs. + /// + public class MsBuildRuleDescription : BaseRuleDescription + { + /// + /// Gets or sets the category of the rule. + /// + public string Category { get; set; } + + /// + /// Gets or sets the identifier of the rule. + /// + public int RuleId { get; set; } + } +} \ No newline at end of file diff --git a/src/Cake.Issues.MsBuild/MsBuildRuleUrlResolver.cs b/src/Cake.Issues.MsBuild/MsBuildRuleUrlResolver.cs new file mode 100644 index 000000000..29d6b8f81 --- /dev/null +++ b/src/Cake.Issues.MsBuild/MsBuildRuleUrlResolver.cs @@ -0,0 +1,88 @@ +namespace Cake.Issues.MsBuild +{ + using System; + using System.Text; + + /// + /// Class for retrieving an URL linking to a site describing a rule. + /// + internal class MsBuildRuleUrlResolver : BaseRuleUrlResolver + { + private static readonly Lazy InstanceValue = + new (() => new MsBuildRuleUrlResolver()); + + /// + /// Initializes a new instance of the class. + /// + private MsBuildRuleUrlResolver() + { + // Add resolver for common known issue categories. + + // .NET SDK analyzers + this.AddUrlResolver(x => + x.Category.Equals("CA", StringComparison.OrdinalIgnoreCase) ? + new Uri("https://www.google.com/search?q=%22" + x.Rule + ":%22+site:learn.microsoft.com") : + null); + + // StyleCop analyzer rules + this.AddUrlResolver(x => + x.Category.Equals("SA", StringComparison.OrdinalIgnoreCase) ? + new Uri("https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/" + x.Rule + ".md") : + null); + + // SonarLint rules + this.AddUrlResolver(x => + x.Category.Equals("S", StringComparison.OrdinalIgnoreCase) ? + new Uri("https://rules.sonarsource.com/csharp/RSPEC-" + x.RuleId) : + null); + + // Roslynator rules + this.AddUrlResolver(x => + x.Category.Equals("RCS", StringComparison.OrdinalIgnoreCase) ? + new Uri("https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/" + x.Rule + ".md") : + null); + } + + /// + /// Gets the instance of the rule resolver. + /// + public static MsBuildRuleUrlResolver Instance => InstanceValue.Value; + + /// + protected override bool TryGetRuleDescription(string rule, MsBuildRuleDescription ruleDescription) + { + // Parse the rule. Expect it in the form starting with a identifier containing characters + // followed by the rule id as a number. + var digitIndex = -1; + var categoryBuilder = new StringBuilder(); + for (var index = 0; index < rule.Length; index++) + { + var currentChar = rule[index]; + if (char.IsDigit(currentChar)) + { + digitIndex = index; + break; + } + + categoryBuilder.Append(currentChar); + } + + // If rule doesn't contain numbers return false. + if (digitIndex < 0) + { + return false; + } + + // Try to parse the part after the first number to an integer. + if (!int.TryParse(rule.AsSpan(digitIndex), out var ruleId)) + { + return false; + } + + ruleDescription.RuleId = ruleId; + ruleDescription.Category = categoryBuilder.ToString(); + + return true; + } + } +} diff --git a/src/Cake.Issues.MsBuild/Properties/ProjectInfo.cs b/src/Cake.Issues.MsBuild/Properties/ProjectInfo.cs new file mode 100644 index 000000000..d46e22d9d --- /dev/null +++ b/src/Cake.Issues.MsBuild/Properties/ProjectInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1a6c74ce-4775-458a-b013-bde1986c5b88")] + +[assembly: CLSCompliant(true)] +[assembly: InternalsVisibleTo("Cake.Issues.MsBuild.Tests")] \ No newline at end of file diff --git a/src/Cake.Issues.sln b/src/Cake.Issues.sln index e6d5b57f6..d6ebf92ee 100644 --- a/src/Cake.Issues.sln +++ b/src/Cake.Issues.sln @@ -30,10 +30,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cake.Issues.Reporting.Tests EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8756998D-64BF-4554-A14F-15AAFE78C8E8}" ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cake.Issues.MsBuild", "Cake.Issues.MsBuild\Cake.Issues.MsBuild.csproj", "{F6B0F4F3-0046-453A-8E1E-FD926DFE22F6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cake.Issues.MsBuild.Tests", "Cake.Issues.MsBuild.Tests\Cake.Issues.MsBuild.Tests.csproj", "{1740BD2B-B317-4CB2-90CD-0AB0F8426AE1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +72,14 @@ Global {07C79C7F-D854-43F3-8C2E-6E8147A78B94}.Debug|Any CPU.Build.0 = Debug|Any CPU {07C79C7F-D854-43F3-8C2E-6E8147A78B94}.Release|Any CPU.ActiveCfg = Release|Any CPU {07C79C7F-D854-43F3-8C2E-6E8147A78B94}.Release|Any CPU.Build.0 = Release|Any CPU + {F6B0F4F3-0046-453A-8E1E-FD926DFE22F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6B0F4F3-0046-453A-8E1E-FD926DFE22F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6B0F4F3-0046-453A-8E1E-FD926DFE22F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6B0F4F3-0046-453A-8E1E-FD926DFE22F6}.Release|Any CPU.Build.0 = Release|Any CPU + {1740BD2B-B317-4CB2-90CD-0AB0F8426AE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1740BD2B-B317-4CB2-90CD-0AB0F8426AE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1740BD2B-B317-4CB2-90CD-0AB0F8426AE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1740BD2B-B317-4CB2-90CD-0AB0F8426AE1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE