diff --git a/nuspec/nuget/Cake.Prca.Issues.EsLint.nuspec b/nuspec/nuget/Cake.Prca.Issues.EsLint.nuspec new file mode 100644 index 0000000..6adfde6 --- /dev/null +++ b/nuspec/nuget/Cake.Prca.Issues.EsLint.nuspec @@ -0,0 +1,23 @@ + + + + Cake.Prca.Issues.EsLint + Cake.Prca.Issues.EsLint + 0.0.0 + BBT Software AG + bbtsoftware, pascalberger, cake-contrib + ESLint support for the Pull Request Code Analysis Addin for Cake Build Automation System + The ESLint support for the Pull Request Code Analysis Addin for Cake allows you to write any issues logged by ESLint as comments to a pull request. + https://github.com/cake-contrib/Cake.Prca.Issues.EsLint/blob/develop/LICENSE + https://github.com/cake-contrib/Cake.Prca.Issues.EsLint + https://raw.githubusercontent.com/cake-build/graphics/aba74fb4cb5fe9454381af2cc70c870088229d5c/png/cake-medium.png + false + Copyright © 2017 BBT Software AG, Root/Zermatt, Switzerland + Cake Script PullRequest CodeAnalysis Cake-Prca-IssueProvider JavaScript Linting ESLint + + + + + + + \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint.Tests.ruleset b/src/Cake.Prca.Issues.EsLint.Tests.ruleset new file mode 100644 index 0000000..4ac6775 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests.ruleset @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint.Tests/Cake.Prca.Issues.EsLint.Tests.csproj b/src/Cake.Prca.Issues.EsLint.Tests/Cake.Prca.Issues.EsLint.Tests.csproj new file mode 100644 index 0000000..966d688 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/Cake.Prca.Issues.EsLint.Tests.csproj @@ -0,0 +1,120 @@ + + + + + + Debug + AnyCPU + {B1625322-FDA2-4E90-A403-8CE8C0748D09} + Library + Properties + Cake.Prca.Issues.EsLint.Tests + Cake.Prca.Issues.EsLint.Tests + v4.5.2 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + ..\Cake.Prca.Issues.EsLint.Tests.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + ..\Cake.Prca.Issues.EsLint.Tests.ruleset + + + + ..\packages\Cake.Core.0.16.2\lib\net45\Cake.Core.dll + True + + + ..\packages\Cake.Prca.0.3.0\lib\net45\Cake.Prca.dll + True + + + ..\packages\Cake.Testing.0.16.2\lib\net45\Cake.Testing.dll + True + + + ..\packages\Shouldly.2.8.2\lib\net451\Shouldly.dll + True + + + + + + + + + + + ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll + True + + + ..\packages\xunit.assert.2.2.0\lib\netstandard1.1\xunit.assert.dll + True + + + ..\packages\xunit.extensibility.core.2.2.0\lib\netstandard1.1\xunit.core.dll + True + + + ..\packages\xunit.extensibility.execution.2.2.0\lib\net452\xunit.execution.desktop.dll + True + + + + + + + + + + + + + + {c6e0bc8d-6bd8-475e-a69f-ced3ffb86362} + Cake.Prca.Issues.EsLint + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint.Tests/EsLintProviderFixture.cs b/src/Cake.Prca.Issues.EsLint.Tests/EsLintProviderFixture.cs new file mode 100644 index 0000000..ac007eb --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/EsLintProviderFixture.cs @@ -0,0 +1,48 @@ +namespace Cake.Prca.Issues.EsLint.Tests +{ + using System.Collections.Generic; + using System.IO; + using Core.Diagnostics; + using Testing; + + public class EsLintProviderFixture + { + public EsLintProviderFixture(string fileResourceName) + { + this.Log = new FakeLog { Verbosity = Verbosity.Normal }; + + using (var stream = this.GetType().Assembly.GetManifestResourceStream("Cake.Prca.Issues.EsLint.Tests.Testfiles." + fileResourceName)) + { + using (var sr = new StreamReader(stream)) + { + this.Settings = + EsLintSettings.FromContent( + sr.ReadToEnd(), + new JsonFormat(this.Log)); + } + } + + this.PrcaSettings = + new ReportCodeAnalysisIssuesToPullRequestSettings(@"c:\Source\Cake.Prca"); + } + + public FakeLog Log { get; set; } + + public EsLintSettings Settings { get; set; } + + public ReportCodeAnalysisIssuesToPullRequestSettings PrcaSettings { get; set; } + + internal EsLintProvider Create() + { + var provider = new EsLintProvider(this.Log, this.Settings); + provider.Initialize(this.PrcaSettings); + return provider; + } + + internal IEnumerable ReadIssues() + { + var codeAnalysisProvider = this.Create(); + return codeAnalysisProvider.ReadIssues(PrcaCommentFormat.PlainText); + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint.Tests/EsLintProviderTests.cs b/src/Cake.Prca.Issues.EsLint.Tests/EsLintProviderTests.cs new file mode 100644 index 0000000..4e16d7c --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/EsLintProviderTests.cs @@ -0,0 +1,41 @@ +namespace Cake.Prca.Issues.EsLint.Tests +{ + using System.Linq; + using Core.IO; + using Shouldly; + using Testing; + using Xunit; + + public class EsLintProviderTests + { + public sealed class TheMsBuildCodeAnalysisProviderCtor + { + [Fact] + public void Should_Throw_If_Log_Is_Null() + { + // Given / When + var result = Record.Exception(() => + new EsLintProvider( + null, + EsLintSettings.FromContent( + "Foo", + new JsonFormat(new FakeLog())))); + + // Then + result.IsArgumentNullException("log"); + } + + [Fact] + public void Should_Throw_If_Settings_Are_Null() + { + var result = Record.Exception(() => + new EsLintProvider( + new FakeLog(), + null)); + + // Then + result.IsArgumentNullException("settings"); + } + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint.Tests/EsLintRuleUrlResolverTests.cs b/src/Cake.Prca.Issues.EsLint.Tests/EsLintRuleUrlResolverTests.cs new file mode 100644 index 0000000..6630c3a --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/EsLintRuleUrlResolverTests.cs @@ -0,0 +1,78 @@ +namespace Cake.Prca.Issues.EsLint.Tests +{ + using System; + using Shouldly; + using Xunit; + + public class EsLintRuleUrlResolverTests + { + public sealed class TheResolveRuleUrlMethod + { + [Fact] + public void Should_Throw_If_Rule_Is_Null() + { + // Given / When + var result = Record.Exception(() => EsLintRuleUrlResolver.Instance.ResolveRuleUrl(null)); + + // Then + result.IsArgumentNullException("rule"); + } + + [Fact] + public void Should_Throw_If_Rule_Is_Empty() + { + // Given / When + var result = Record.Exception(() => EsLintRuleUrlResolver.Instance.ResolveRuleUrl(string.Empty)); + + // Then + result.IsArgumentOutOfRangeException("rule"); + } + + [Fact] + public void Should_Throw_If_Rule_Is_WhiteSpace() + { + // Given / When + var result = Record.Exception(() => EsLintRuleUrlResolver.Instance.ResolveRuleUrl(" ")); + + // Then + result.IsArgumentOutOfRangeException("rule"); + } + + [Theory] + [InlineData("no-unused-vars", "http://eslint.org/docs/rules/no-unused-vars")] + [InlineData("no-await-in-loop", "http://eslint.org/docs/rules/no-await-in-loop")] + public void Should_Resolve_Url(string rule, string expectedUrl) + { + // Given + var urlResolver = EsLintRuleUrlResolver.Instance; + + // When + var ruleUrl = urlResolver.ResolveRuleUrl(rule); + + // Then + ruleUrl.ToString().ShouldBe(expectedUrl); + } + + [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 = EsLintRuleUrlResolver.Instance; + urlResolver.AddUrlResolver(x => x.Rule == foo ? new Uri(fooUrl) : null, 1); + urlResolver.AddUrlResolver(x => x.Rule == bar ? new Uri(barUrl) : null, 1); + + // 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.Prca.Issues.EsLint.Tests/EsLintSettingsTests.cs b/src/Cake.Prca.Issues.EsLint.Tests/EsLintSettingsTests.cs new file mode 100644 index 0000000..80e3a79 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/EsLintSettingsTests.cs @@ -0,0 +1,162 @@ +namespace Cake.Prca.Issues.EsLint.Tests +{ + using System; + using System.IO; + using System.Linq; + using System.Text; + using Shouldly; + using Testing; + using Xunit; + + public class EsLintSettingsTests + { + public sealed class TheEsLintSettingsCtor + { + [Fact] + public void Should_Throw_If_LogFilePath_Is_Null() + { + // Given / When + var result = Record.Exception(() => + EsLintSettings.FromFilePath( + null, + new JsonFormat(new FakeLog()))); + + // Then + result.IsArgumentNullException("logFilePath"); + } + + [Fact] + public void Should_Throw_If_Format_For_LogFilePath_Is_Null() + { + // Given / When + var result = Record.Exception(() => + EsLintSettings.FromFilePath( + @"C:\foo.log", + null)); + + // Then + result.IsArgumentNullException("format"); + } + + [Fact] + public void Should_Throw_If_LogFileContent_Is_Null() + { + // Given / When + var result = Record.Exception(() => + EsLintSettings.FromContent( + null, + new JsonFormat(new FakeLog()))); + + // Then + result.IsArgumentNullException("logFileContent"); + } + + [Fact] + public void Should_Throw_If_LogFileContent_Is_Empty() + { + // Given / When + var result = Record.Exception(() => + EsLintSettings.FromContent( + string.Empty, + new JsonFormat(new FakeLog()))); + + // Then + result.IsArgumentOutOfRangeException("logFileContent"); + } + + [Fact] + public void Should_Throw_If_LogFileContent_Is_WhiteSpace() + { + // Given / When + var result = Record.Exception(() => + EsLintSettings.FromContent( + " ", + new JsonFormat(new FakeLog()))); + + // Then + result.IsArgumentOutOfRangeException("logFileContent"); + } + + [Fact] + public void Should_Throw_If_Format_For_LogFileContent_Is_Null() + { + // Given / When + var result = Record.Exception(() => + EsLintSettings.FromContent( + "foo", + null)); + + // Then + result.IsArgumentNullException("format"); + } + + [Fact] + public void Should_Set_Property_Values_Passed_To_Constructor() + { + // Given + const string logFileContent = "foo"; + var format = new JsonFormat(new FakeLog()); + + // When + var settings = EsLintSettings.FromContent(logFileContent, format); + + // Then + settings.LogFileContent.ShouldBe(logFileContent); + settings.Format.ShouldBe(format); + } + + [Fact] + public void Should_Read_File_From_Disk() + { + var fileName = Path.GetTempFileName(); + try + { + // Given + string expected; + using (var ms = new MemoryStream()) + using (var stream = this.GetType().Assembly.GetManifestResourceStream("Cake.Prca.Issues.EsLint.Tests.Testfiles.jsonFormatWindows.json")) + { + stream.CopyTo(ms); + var data = ms.ToArray(); + + using (var file = new FileStream(fileName, FileMode.Create, FileAccess.Write)) + { + file.Write(data, 0, data.Length); + } + + expected = ConvertFromUtf8(data); + } + + // When + var settings = + EsLintSettings.FromFilePath( + fileName, + new JsonFormat(new FakeLog())); + + // Then + settings.LogFileContent.ShouldBe(expected); + } + finally + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } + } + + private static string ConvertFromUtf8(byte[] bytes) + { + var enc = new UTF8Encoding(true); + var preamble = enc.GetPreamble(); + + if (preamble.Where((p, i) => p != bytes[i]).Any()) + { + throw new ArgumentException("Not utf8-BOM"); + } + + return enc.GetString(bytes.Skip(preamble.Length).ToArray()); + } + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint.Tests/ExceptionAssertExtensions.cs b/src/Cake.Prca.Issues.EsLint.Tests/ExceptionAssertExtensions.cs new file mode 100644 index 0000000..a09e148 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/ExceptionAssertExtensions.cs @@ -0,0 +1,20 @@ +namespace Cake.Prca.Issues.EsLint.Tests +{ + using System; + using Xunit; + + public static class ExceptionAssertExtensions + { + public static void IsArgumentNullException(this Exception exception, string parameterName) + { + Assert.IsType(exception); + Assert.Equal(parameterName, ((ArgumentNullException)exception).ParamName); + } + + public static void IsArgumentOutOfRangeException(this Exception exception, string parameterName) + { + Assert.IsType(exception); + Assert.Equal(parameterName, ((ArgumentOutOfRangeException)exception).ParamName); + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint.Tests/JsonFormatTests.cs b/src/Cake.Prca.Issues.EsLint.Tests/JsonFormatTests.cs new file mode 100644 index 0000000..ee4d280 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/JsonFormatTests.cs @@ -0,0 +1,146 @@ +namespace Cake.Prca.Issues.EsLint.Tests +{ + using System.Linq; + using Core.IO; + using Shouldly; + using Xunit; + + public class JsonFormatTests + { + public sealed class TheJsonFormatCtor + { + [Fact] + public void Should_Throw_If_Log_Is_Null() + { + // Given / When + var result = Record.Exception(() => new JsonFormat(null)); + + // Then + result.IsArgumentNullException("log"); + } + } + + public sealed class TheReadIssuesMethod + { + [Fact] + public void Should_Read_Issue_Correct() + { + // Given + var fixture = new EsLintProviderFixture("jsonFormatWindows.json"); + + // When + var issues = fixture.ReadIssues().ToList(); + + // Then + issues.Count.ShouldBe(9); + CheckIssue( + issues[0], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 1, + "no-unused-vars", + "http://eslint.org/docs/rules/no-unused-vars", + 2, + "'addOne' is defined but never used."); + CheckIssue( + issues[1], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 2, + "use-isnan", + "http://eslint.org/docs/rules/use-isnan", + 2, + "Use the isNaN function to compare with NaN."); + CheckIssue( + issues[2], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 3, + "space-unary-ops", + "http://eslint.org/docs/rules/space-unary-ops", + 2, + "Unexpected space before unary operator '++'."); + CheckIssue( + issues[3], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 3, + "semi", + "http://eslint.org/docs/rules/semi", + 1, + "Missing semicolon."); + CheckIssue( + issues[4], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 4, + "no-else-return", + "http://eslint.org/docs/rules/no-else-return", + 1, + "Unnecessary 'else' after 'return'."); + CheckIssue( + issues[5], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 5, + "indent", + "http://eslint.org/docs/rules/indent", + 1, + "Expected indentation of 8 spaces but found 6."); + CheckIssue( + issues[6], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 5, + "consistent-return", + "http://eslint.org/docs/rules/consistent-return", + 2, + "Function 'addOne' expected a return value."); + CheckIssue( + issues[7], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 5, + "semi", + "http://eslint.org/docs/rules/semi", + 1, + "Missing semicolon."); + CheckIssue( + issues[8], + @"src\Cake.Prca.Issues.EsLint.Tests\Testfiles\fullOfProblems.js", + 7, + "no-extra-semi", + "http://eslint.org/docs/rules/no-extra-semi", + 2, + "Unnecessary semicolon."); + } + + private static void CheckIssue( + ICodeAnalysisIssue issue, + string affectedFileRelativePath, + int? line, + string rule, + string ruleUrl, + int priority, + string message) + { + if (issue.AffectedFileRelativePath == null) + { + affectedFileRelativePath.ShouldBeNull(); + } + else + { + issue.AffectedFileRelativePath.ToString().ShouldBe(new FilePath(affectedFileRelativePath).ToString()); + issue.AffectedFileRelativePath.IsRelative.ShouldBe(true, "Issue path is not relative"); + } + + issue.Line.ShouldBe(line); + issue.Rule.ShouldBe(rule); + + if (issue.RuleUrl == null) + { + ruleUrl.ShouldBeNull(); + } + else + { + issue.RuleUrl.ToString().ShouldBe(ruleUrl); + } + + issue.Priority.ShouldBe(priority); + issue.Message.ShouldBe(message); + } + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint.Tests/Properties/AssemblyInfo.cs b/src/Cake.Prca.Issues.EsLint.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..32dedc7 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ESLint support for Cake Pull Request Code Analysis Testcases")] +[assembly: AssemblyDescription("ESLint support for the Pull Request Code Analysis Addin for Cake Build Automation System")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("BBT Software AG")] +[assembly: AssemblyProduct("Cake Pull Request Code Analysis Addin")] +[assembly: AssemblyCopyright("Copyright © 2017 BBT Software AG, Root/Zermatt, Switzerland")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 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("b1625322-fda2-4e90-a403-8ce8c0748d09")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Cake.Prca.Issues.EsLint.Tests/Testfiles/jsonFormatWindows.json b/src/Cake.Prca.Issues.EsLint.Tests/Testfiles/jsonFormatWindows.json new file mode 100644 index 0000000..6291fcb --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/Testfiles/jsonFormatWindows.json @@ -0,0 +1,119 @@ +[ + { + "filePath": "c:\\Source\\Cake.Prca\\src\\Cake.Prca.Issues.EsLint.Tests\\Testfiles\\fullOfProblems.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'addOne' is defined but never used.", + "line": 1, + "column": 10, + "nodeType": "Identifier", + "source": "function addOne(i) {" + }, + { + "ruleId": "use-isnan", + "severity": 2, + "message": "Use the isNaN function to compare with NaN.", + "line": 2, + "column": 9, + "nodeType": "BinaryExpression", + "source": " if (i != NaN) {" + }, + { + "ruleId": "space-unary-ops", + "severity": 2, + "message": "Unexpected space before unary operator '++'.", + "line": 3, + "column": 16, + "nodeType": "UpdateExpression", + "source": " return i ++", + "fix": { + "range": [ 57, 58 ], + "text": "" + } + }, + { + "ruleId": "semi", + "severity": 1, + "message": "Missing semicolon.", + "line": 3, + "column": 20, + "nodeType": "ReturnStatement", + "source": " return i ++", + "fix": { + "range": [ 60, 60 ], + "text": ";" + } + }, + { + "ruleId": "no-else-return", + "severity": 1, + "message": "Unnecessary 'else' after 'return'.", + "line": 4, + "column": 12, + "nodeType": "BlockStatement", + "source": " } else {", + "fix": { + "range": [ 0, 94 ], + "text": "function addOne(i) {\n if (i != NaN) {\n return i ++\n } \n return\n \n}" + } + }, + { + "ruleId": "indent", + "severity": 1, + "message": "Expected indentation of 8 spaces but found 6.", + "line": 5, + "column": 1, + "nodeType": "Keyword", + "source": " return", + "endLine": 5, + "endColumn": 7, + "fix": { + "range": [ 74, 80 ], + "text": " " + } + }, + { + "ruleId": "consistent-return", + "severity": 2, + "message": "Function 'addOne' expected a return value.", + "line": 5, + "column": 7, + "nodeType": "ReturnStatement", + "source": " return" + }, + { + "ruleId": "semi", + "severity": 1, + "message": "Missing semicolon.", + "line": 5, + "column": 13, + "nodeType": "ReturnStatement", + "source": " return", + "fix": { + "range": [ 86, 86 ], + "text": ";" + } + }, + { + "ruleId": "no-extra-semi", + "severity": 2, + "message": "Unnecessary semicolon.", + "line": 7, + "column": 2, + "nodeType": "EmptyStatement", + "source": "};", + "fix": { + "range": [ 93, 95 ], + "text": "}" + } + } + ], + "errorCount": 5, + "warningCount": 4, + "fixableErrorCount": 2, + "fixableWarningCount": 4, + "source": "function addOne(i) {\n if (i != NaN) {\n return i ++\n } else {\n return\n }\n};" + } +] \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint.Tests/packages.config b/src/Cake.Prca.Issues.EsLint.Tests/packages.config new file mode 100644 index 0000000..dc8ca5d --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.Tests/packages.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint.ruleset b/src/Cake.Prca.Issues.EsLint.ruleset new file mode 100644 index 0000000..f8d6df1 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.ruleset @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint.sln b/src/Cake.Prca.Issues.EsLint.sln new file mode 100644 index 0000000..f0afb9c --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cake.Prca.Issues.EsLint", "Cake.Prca.Issues.EsLint\Cake.Prca.Issues.EsLint.csproj", "{C6E0BC8D-6BD8-475E-A69F-CED3FFB86362}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cake.Prca.Issues.EsLint.Tests", "Cake.Prca.Issues.EsLint.Tests\Cake.Prca.Issues.EsLint.Tests.csproj", "{B1625322-FDA2-4E90-A403-8CE8C0748D09}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{0C2D356A-09A1-4502-89D9-65C675803DCD}" + ProjectSection(SolutionItems) = preProject + ..\build.ps1 = ..\build.ps1 + ..\build.sh = ..\build.sh + ..\setup.cake = ..\setup.cake + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuspec", "nuspec", "{B4C4F0ED-9268-41C7-A936-AF34D83DB404}" + ProjectSection(SolutionItems) = preProject + ..\nuspec\nuget\Cake.Prca.Issues.EsLint.nuspec = ..\nuspec\nuget\Cake.Prca.Issues.EsLint.nuspec + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C6E0BC8D-6BD8-475E-A69F-CED3FFB86362}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6E0BC8D-6BD8-475E-A69F-CED3FFB86362}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6E0BC8D-6BD8-475E-A69F-CED3FFB86362}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6E0BC8D-6BD8-475E-A69F-CED3FFB86362}.Release|Any CPU.Build.0 = Release|Any CPU + {B1625322-FDA2-4E90-A403-8CE8C0748D09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1625322-FDA2-4E90-A403-8CE8C0748D09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1625322-FDA2-4E90-A403-8CE8C0748D09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1625322-FDA2-4E90-A403-8CE8C0748D09}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B4C4F0ED-9268-41C7-A936-AF34D83DB404} = {0C2D356A-09A1-4502-89D9-65C675803DCD} + EndGlobalSection +EndGlobal diff --git a/src/Cake.Prca.Issues.EsLint.sln.DotSettings b/src/Cake.Prca.Issues.EsLint.sln.DotSettings new file mode 100644 index 0000000..7cc51a4 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint.sln.DotSettings @@ -0,0 +1,3 @@ + + DO_NOT_SHOW + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint/Cake.Prca.Issues.EsLint.csproj b/src/Cake.Prca.Issues.EsLint/Cake.Prca.Issues.EsLint.csproj new file mode 100644 index 0000000..75f9f54 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/Cake.Prca.Issues.EsLint.csproj @@ -0,0 +1,106 @@ + + + + + Debug + AnyCPU + {C6E0BC8D-6BD8-475E-A69F-CED3FFB86362} + Library + Properties + Cake.Prca.Issues.EsLint + Cake.Prca.Issues.EsLint + v4.5.2 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + ..\Cake.Prca.Issues.EsLint.ruleset + bin\Debug\Cake.Prca.Issues.EsLint.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + ..\Cake.Prca.Issues.EsLint.ruleset + bin\Release\Cake.Prca.Issues.EsLint.XML + + + + ..\packages\Cake.Core.0.16.2\lib\net45\Cake.Core.dll + True + + + ..\packages\Cake.Prca.0.3.0\lib\net45\Cake.Prca.dll + True + + + ..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint/EsLintProvider.cs b/src/Cake.Prca.Issues.EsLint/EsLintProvider.cs new file mode 100644 index 0000000..818afeb --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/EsLintProvider.cs @@ -0,0 +1,32 @@ +namespace Cake.Prca.Issues.EsLint +{ + using System.Collections.Generic; + using Core.Diagnostics; + + /// + /// Provider for code analysis issues reported by ESLint. + /// + internal class EsLintProvider : CodeAnalysisProvider + { + private readonly EsLintSettings settings; + + /// + /// Initializes a new instance of the class. + /// + /// The Cake log context. + /// Settings for reading the log file. + public EsLintProvider(ICakeLog log, EsLintSettings settings) + : base(log) + { + settings.NotNull(nameof(settings)); + + this.settings = settings; + } + + /// + protected override IEnumerable InternalReadIssues(PrcaCommentFormat format) + { + return this.settings.Format.ReadIssues(this.PrcaSettings, this.settings); + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint/EsLintProviderAliases.cs b/src/Cake.Prca.Issues.EsLint/EsLintProviderAliases.cs new file mode 100644 index 0000000..49af2b4 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/EsLintProviderAliases.cs @@ -0,0 +1,208 @@ +namespace Cake.Prca.Issues.EsLint +{ + using System; + using Core; + using Core.Annotations; + using Core.IO; + + /// + /// Contains functionality related to importing code analysis issues from ESLint + /// to write them to pull requests. + /// + [CakeAliasCategory(CakeAliasConstants.MainCakeAliasCategory)] + [CakeNamespaceImport("Cake.Prca.Issues.EsLint")] + public static class EsLintProviderAliases + { + /// + /// 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 starting + /// with the string Foo to search with Google for the rule: + /// + /// + /// x.Rule.StartsWith("Foo") ? + /// new Uri("https://www.google.com/search?q=%22" + x.Rule + "%22") : + /// null) + /// ]]> + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(CakeAliasConstants.CodeAnalysisProviderCakeAliasCategory)] + public static void EsLintAddRuleUrlResolver( + this ICakeContext context, + Func resolver) + { + context.NotNull(nameof(context)); + resolver.NotNull(nameof(resolver)); + + EsLintRuleUrlResolver.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 starting with the string + /// Foo to search with Google for the rule: + /// + /// + /// x.Rule.StartsWith("Foo") ? + /// new Uri("https://www.google.com/search?q=%22" + x.Rule + "%22") : + /// null, + /// 5) + /// ]]> + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(CakeAliasConstants.CodeAnalysisProviderCakeAliasCategory)] + public static void EsLintAddRuleUrlResolver( + this ICakeContext context, + Func resolver, + int priority) + { + context.NotNull(nameof(context)); + resolver.NotNull(nameof(resolver)); + + EsLintRuleUrlResolver.Instance.AddUrlResolver(resolver, priority); + } + + /// + /// Gets an instance for the ESLint JSON log format as written by the JSON formatter + /// + /// The context. + /// Instance for the ESLint JSON log format. + [CakePropertyAlias] + [CakeAliasCategory(CakeAliasConstants.CodeAnalysisProviderCakeAliasCategory)] + public static ILogFileFormat EsLintJsonFormat( + this ICakeContext context) + { + context.NotNull(nameof(context)); + + return new JsonFormat(context.Log); + } + + /// + /// Gets an instance of a provider for code analysis issues reported by ESLint using a log file from disk. + /// + /// The context. + /// Path to the the ESLint log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided ESLint log file. + /// Instance of a provider for code analysis issues reported by ESLint. + /// + /// Report code analysis issues reported by ESLint to a TFS pull request: + /// + /// + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(CakeAliasConstants.CodeAnalysisProviderCakeAliasCategory)] + public static ICodeAnalysisProvider EsLintFromFilePath( + this ICakeContext context, + FilePath logFilePath, + ILogFileFormat format) + { + context.NotNull(nameof(context)); + logFilePath.NotNull(nameof(logFilePath)); + format.NotNull(nameof(format)); + + return context.EsLint(EsLintSettings.FromFilePath(logFilePath, format)); + } + + /// + /// Gets an instance of a provider for code analysis issues reported by ESLint using log file content. + /// + /// The context. + /// Content of the the ESLint log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided ESLint log file. + /// Instance of a provider for code analysis issues reported by ESLint. + /// + /// Report code analysis issues reported by ESLint to a TFS pull request: + /// + /// + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(CakeAliasConstants.CodeAnalysisProviderCakeAliasCategory)] + public static ICodeAnalysisProvider EsLintFromContent( + this ICakeContext context, + string logFileContent, + ILogFileFormat format) + { + context.NotNull(nameof(context)); + logFileContent.NotNullOrWhiteSpace(nameof(logFileContent)); + format.NotNull(nameof(format)); + + return context.EsLint(EsLintSettings.FromContent(logFileContent, format)); + } + + /// + /// Gets an instance of a provider for code analysis issues reported by ESLint using specified settings. + /// + /// The context. + /// Settings for reading the ESLint log. + /// Instance of a provider for code analysis issues reported by ESLint. + /// + /// Report code analysis issues reported by ESLint to a TFS pull request: + /// + /// + /// + /// + [CakeMethodAlias] + [CakeAliasCategory(CakeAliasConstants.CodeAnalysisProviderCakeAliasCategory)] + public static ICodeAnalysisProvider EsLint( + this ICakeContext context, + EsLintSettings settings) + { + context.NotNull(nameof(context)); + settings.NotNull(nameof(settings)); + + return new EsLintProvider(context.Log, settings); + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint/EsLintRuleUrlResolver.cs b/src/Cake.Prca.Issues.EsLint/EsLintRuleUrlResolver.cs new file mode 100644 index 0000000..a62c5a3 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/EsLintRuleUrlResolver.cs @@ -0,0 +1,33 @@ +namespace Cake.Prca.Issues.EsLint +{ + using System; + + /// + /// Class for retrieving an URL linking to a site describing a rule. + /// + internal class EsLintRuleUrlResolver : BaseRuleUrlResolver + { + private static readonly Lazy InstanceValue = + new Lazy(() => new EsLintRuleUrlResolver()); + + /// + /// Initializes a new instance of the class. + /// + private EsLintRuleUrlResolver() + { + this.AddUrlResolver(x => + new Uri("http://eslint.org/docs/rules/" + x.Rule)); + } + + /// + /// Gets the instance of the rule resolver. + /// + public static EsLintRuleUrlResolver Instance => InstanceValue.Value; + + /// + protected override bool TryGetRuleDescription(string rule, BaseRuleDescription ruleDescription) + { + return true; + } + } +} \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint/EsLintSettings.cs b/src/Cake.Prca.Issues.EsLint/EsLintSettings.cs new file mode 100644 index 0000000..a6eed76 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/EsLintSettings.cs @@ -0,0 +1,83 @@ +namespace Cake.Prca.Issues.EsLint +{ + using System.IO; + using Core.IO; + + /// + /// Settings for . + /// + public class EsLintSettings + { + /// + /// Initializes a new instance of the class. + /// + /// Path to the the ESLint log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided ESLint log file. + protected EsLintSettings(FilePath logFilePath, ILogFileFormat format) + { + logFilePath.NotNull(nameof(logFilePath)); + format.NotNull(nameof(format)); + + this.Format = format; + + using (var stream = new FileStream(logFilePath.FullPath, FileMode.Open, FileAccess.Read)) + { + using (var sr = new StreamReader(stream)) + { + this.LogFileContent = sr.ReadToEnd(); + } + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Content of the the ESLint log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided ESLint log file. + protected EsLintSettings(string logFileContent, ILogFileFormat format) + { + logFileContent.NotNullOrWhiteSpace(nameof(logFileContent)); + format.NotNull(nameof(format)); + + this.LogFileContent = logFileContent; + this.Format = format; + } + + /// + /// Gets the format of the MsBuild log file. + /// + public ILogFileFormat Format { get; private set; } + + /// + /// Gets the content of the log file. + /// + public string LogFileContent { get; private set; } + + /// + /// Returns a new instance of the class from a log file on disk. + /// + /// Path to the ESLint log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided ESLint log file. + /// Instance of the class. + public static EsLintSettings FromFilePath(FilePath logFilePath, ILogFileFormat format) + { + return new EsLintSettings(logFilePath, format); + } + + /// + /// Returns a new instance of the class from the content + /// of a ESLint log file. + /// + /// Content of the ESLint log file. + /// The log file needs to be in the format as defined by the parameter. + /// Format of the provided ESLint log file. + /// Instance of the class. + public static EsLintSettings FromContent(string logFileContent, ILogFileFormat format) + { + return new EsLintSettings(logFileContent, format); + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint/FodyWeavers.xml b/src/Cake.Prca.Issues.EsLint/FodyWeavers.xml new file mode 100644 index 0000000..e0a35bd --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Cake.Prca.Issues.EsLint/ILogFileFormat.cs b/src/Cake.Prca.Issues.EsLint/ILogFileFormat.cs new file mode 100644 index 0000000..230d3ab --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/ILogFileFormat.cs @@ -0,0 +1,20 @@ +namespace Cake.Prca.Issues.EsLint +{ + using System.Collections.Generic; + + /// + /// Definition of a ESLint log file format. + /// + public interface ILogFileFormat + { + /// + /// Gets all code analysis issues. + /// + /// General settings to use. + /// Settings for code analysis provider to use. + /// List of code analysis issues + IEnumerable ReadIssues( + ReportCodeAnalysisIssuesToPullRequestSettings prcaSettings, + EsLintSettings settings); + } +} diff --git a/src/Cake.Prca.Issues.EsLint/JsonFormat.cs b/src/Cake.Prca.Issues.EsLint/JsonFormat.cs new file mode 100644 index 0000000..fe74003 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/JsonFormat.cs @@ -0,0 +1,66 @@ +namespace Cake.Prca.Issues.EsLint +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Core.Diagnostics; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// ESLint Json format. + /// + internal class JsonFormat : LogFileFormat + { + /// + /// Initializes a new instance of the class. + /// + /// The Cake log instance. + public JsonFormat(ICakeLog log) + : base(log) + { + } + + /// + public override IEnumerable ReadIssues( + ReportCodeAnalysisIssuesToPullRequestSettings prcaSettings, + EsLintSettings settings) + { + prcaSettings.NotNull(nameof(prcaSettings)); + settings.NotNull(nameof(settings)); + + var logFileEntries = + JsonConvert.DeserializeObject>(settings.LogFileContent); + + return + from file in logFileEntries + from message in file.SelectToken("messages") + let + rule = (string)message.SelectToken("ruleId") + select + new CodeAnalysisIssue( + GetRelativeFilePath((string)file.SelectToken("filePath"), prcaSettings), + (int)message.SelectToken("line"), + (string)message.SelectToken("message"), + (int)message.SelectToken("severity"), + rule, + EsLintRuleUrlResolver.Instance.ResolveRuleUrl(rule)); + } + + private static string GetRelativeFilePath( + string absoluteFilePath, + ReportCodeAnalysisIssuesToPullRequestSettings prcaSettings) + { + // Make path relative to repository root. + var relativeFilePath = absoluteFilePath.Substring(prcaSettings.RepositoryRoot.FullPath.Length); + + // Remove leading directory separator. + if (relativeFilePath.StartsWith(Path.DirectorySeparatorChar.ToString())) + { + relativeFilePath = relativeFilePath.Substring(1); + } + + return relativeFilePath; + } + } +} diff --git a/src/Cake.Prca.Issues.EsLint/LogFileFormat.cs b/src/Cake.Prca.Issues.EsLint/LogFileFormat.cs new file mode 100644 index 0000000..fd7a7a7 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/LogFileFormat.cs @@ -0,0 +1,32 @@ +namespace Cake.Prca.Issues.EsLint +{ + using System.Collections.Generic; + using Core.Diagnostics; + + /// + /// Base class for all ESLint log file format implementations. + /// + public abstract class LogFileFormat : ILogFileFormat + { + /// + /// Initializes a new instance of the class. + /// + /// The Cake log instance. + protected LogFileFormat(ICakeLog log) + { + log.NotNull(nameof(log)); + + this.Log = log; + } + + /// + /// Gets the Cake log instance. + /// + protected ICakeLog Log { get; private set; } + + /// + public abstract IEnumerable ReadIssues( + ReportCodeAnalysisIssuesToPullRequestSettings prcaSettings, + EsLintSettings settings); + } +} diff --git a/src/Cake.Prca.Issues.EsLint/Properties/AssemblyInfo.cs b/src/Cake.Prca.Issues.EsLint/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a09160f --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ESLint support for Cake Pull Request Code Analysis")] +[assembly: AssemblyDescription("ESLint support for the Pull Request Code Analysis Addin for Cake Build Automation System")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("BBT Software AG")] +[assembly: AssemblyProduct("Cake Pull Request Code Analysis Addin")] +[assembly: AssemblyCopyright("Copyright © 2017 BBT Software AG, Root/Zermatt, Switzerland")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 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("c6e0bc8d-6bd8-475e-a69f-ced3ffb86362")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: CLSCompliant(true)] +[assembly: InternalsVisibleTo("Cake.Prca.Issues.EsLint.Tests")] diff --git a/src/Cake.Prca.Issues.EsLint/packages.config b/src/Cake.Prca.Issues.EsLint/packages.config new file mode 100644 index 0000000..2097df5 --- /dev/null +++ b/src/Cake.Prca.Issues.EsLint/packages.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file