diff --git a/src/Cake.Issues.Tests/IssueBuilderTests.cs b/src/Cake.Issues.Tests/IssueBuilderTests.cs index 64ac78dc1..a4e7e353d 100644 --- a/src/Cake.Issues.Tests/IssueBuilderTests.cs +++ b/src/Cake.Issues.Tests/IssueBuilderTests.cs @@ -2379,4 +2379,106 @@ public void Should_Throw_If_AdditionalInformation_Is_Null() result.IsArgumentNullException("additionalInformation"); } } + + public sealed class TheInFileAtOffsetMethod + { + [Fact] + public void Should_Set_Offset() + { + // Given + var fixture = new IssueBuilderFixture(); + + // When + var issue = fixture.IssueBuilder.InFileAtOffset("foo", 42).Create(); + + // Then + issue.AffectedFileRelativePath.ToString().ShouldBe("foo"); + issue.Offset.ShouldBe(42); + } + + [Fact] + public void Should_Set_Offset_Range() + { + // Given + var fixture = new IssueBuilderFixture(); + + // When + var issue = fixture.IssueBuilder.InFileAtOffset("foo", 10, 20).Create(); + + // Then + issue.AffectedFileRelativePath.ToString().ShouldBe("foo"); + issue.Offset.ShouldBe(10); + issue.EndOffset.ShouldBe(20); + } + + [Fact] + public void Should_Handle_Null_Offset() + { + // Given + var fixture = new IssueBuilderFixture(); + + // When + var issue = fixture.IssueBuilder.InFileAtOffset("foo", null).Create(); + + // Then + issue.AffectedFileRelativePath.ToString().ShouldBe("foo"); + issue.Offset.ShouldBeNull(); + } + + [Fact] + public void Should_Throw_If_Offset_Is_Zero() + { + // Given + var fixture = new IssueBuilderFixture(); + + // When + var result = Record.Exception(() => + fixture.IssueBuilder.InFileAtOffset("foo", 0)); + + // Then + result.IsArgumentOutOfRangeException("offset"); + } + + [Fact] + public void Should_Throw_If_Offset_Is_Negative() + { + // Given + var fixture = new IssueBuilderFixture(); + + // When + var result = Record.Exception(() => + fixture.IssueBuilder.InFileAtOffset("foo", -1)); + + // Then + result.IsArgumentOutOfRangeException("offset"); + } + + [Fact] + public void Should_Throw_If_EndOffset_Is_Zero() + { + // Given + var fixture = new IssueBuilderFixture(); + + // When + var result = Record.Exception(() => + fixture.IssueBuilder.InFileAtOffset("foo", 1, 0)); + + // Then + result.IsArgumentOutOfRangeException("endOffset"); + } + + [Fact] + public void Should_Throw_If_EndOffset_Is_Negative() + { + // Given + var fixture = new IssueBuilderFixture(); + + // When + var result = Record.Exception(() => + fixture.IssueBuilder.InFileAtOffset("foo", 1, -1)); + + // Then + result.IsArgumentOutOfRangeException("endOffset"); + } + } } \ No newline at end of file diff --git a/src/Cake.Issues.Tests/IssueTests.cs b/src/Cake.Issues.Tests/IssueTests.cs index c4e2a75b7..22a82fefa 100644 --- a/src/Cake.Issues.Tests/IssueTests.cs +++ b/src/Cake.Issues.Tests/IssueTests.cs @@ -45,6 +45,8 @@ public void Should_Throw_If_Identifier_Is_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -100,6 +102,8 @@ public void Should_Throw_If_Identifier_Is_Empty() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -155,6 +159,8 @@ public void Should_Throw_If_Identifier_Is_WhiteSpace() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -210,6 +216,8 @@ public void Should_Set_Identifier(string identifier) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -268,6 +276,8 @@ public void Should_Throw_If_Project_Path_Is_Invalid(string projectPath) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -324,6 +334,8 @@ public void Should_Throw_If_File_Path_Is_Absolute(string projectPath) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -382,6 +394,8 @@ public void Should_Throw_If_File_Path_Is_Absolute_Windows_Path(string projectPat endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -437,6 +451,8 @@ public void Should_Handle_Project_Paths_Which_Are_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -492,6 +508,8 @@ public void Should_Handle_Project_Paths_Which_Are_Empty() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -547,6 +565,8 @@ public void Should_Handle_Project_Paths_Which_Are_WhiteSpace() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -602,6 +622,8 @@ public void Should_Set_ProjectFileRelativePath(string projectPath) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -660,6 +682,8 @@ public void Should_Handle_Projects_Which_Are_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -715,6 +739,8 @@ public void Should_Handle_Projects_Which_Are_Empty() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -770,6 +796,8 @@ public void Should_Handle_Projects_Which_Are_WhiteSpace() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -825,6 +853,8 @@ public void Should_Set_ProjectName(string projectName) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -883,6 +913,8 @@ public void Should_Throw_If_File_Path_Is_Invalid(string filePath) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -939,6 +971,8 @@ public void Should_Throw_If_File_Path_Is_Absolute(string filePath) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -997,6 +1031,8 @@ public void Should_Throw_If_File_Path_Is_Absolute_Windows_Path(string filePath) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1052,6 +1088,8 @@ public void Should_Handle_File_Paths_Which_Are_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1107,6 +1145,8 @@ public void Should_Handle_File_Paths_Which_Are_Empty() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1162,6 +1202,8 @@ public void Should_Handle_File_Paths_Which_Are_WhiteSpace() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1225,6 +1267,8 @@ public void Should_Set_File_Path(string filePath, string expectedFilePath) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1284,6 +1328,8 @@ public void Should_Throw_If_Line_Is_Negative() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1339,6 +1385,8 @@ public void Should_Throw_If_Line_Is_Zero() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1394,6 +1442,8 @@ public void Should_Throw_If_Line_Is_Set_But_No_File() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1449,6 +1499,8 @@ public void Should_Handle_Line_Which_Is_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1505,6 +1557,8 @@ public void Should_Set_Line(int line) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1563,6 +1617,8 @@ public void Should_Throw_If_EndLine_Is_Negative() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1618,6 +1674,8 @@ public void Should_Throw_If_EndLine_Is_Zero() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1673,6 +1731,8 @@ public void Should_Throw_If_EndLine_Is_Set_But_No_Line() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1728,6 +1788,8 @@ public void Should_Throw_If_EndLine_Is_Smaller_Line() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1783,6 +1845,8 @@ public void Should_Handle_EndLine_Which_Is_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1838,6 +1902,8 @@ public void Should_Handle_EndLine_Which_Is_Equals_Line() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1894,6 +1960,8 @@ public void Should_Set_EndLine(int endLine) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -1952,6 +2020,8 @@ public void Should_Throw_If_Column_Is_Negative() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2007,6 +2077,8 @@ public void Should_Throw_If_Column_Is_Zero() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2062,6 +2134,8 @@ public void Should_Throw_If_Column_Is_Set_But_No_Line() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2117,6 +2191,8 @@ public void Should_Handle_Column_Which_Is_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2174,6 +2250,8 @@ public void Should_Set_Column(int? column) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2232,6 +2310,8 @@ public void Should_Throw_If_EndColumn_Is_Negative() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2287,6 +2367,8 @@ public void Should_Throw_If_EndColumn_Is_Zero() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2342,6 +2424,8 @@ public void Should_Throw_If_EndColumn_Is_Set_But_No_Column() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2397,6 +2481,8 @@ public void Should_Throw_If_EndColumn_Is_Smaller_Column() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2452,6 +2538,8 @@ public void Should_Handle_EndColumn_Which_Is_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2507,6 +2595,8 @@ public void Should_Handle_EndColumn_Which_Is_Equals_Column() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2562,6 +2652,8 @@ public void Should_Handle_EndColumn_Which_Is_Smaller_Than_Column_If_EndLine_Is_H endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2619,6 +2711,8 @@ public void Should_Set_EndColumn(int? endColumn) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2677,6 +2771,8 @@ public void Should_Set_FileLink() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2732,6 +2828,8 @@ public void Should_Set_FileLink_If_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2790,6 +2888,8 @@ public void Should_Throw_If_MessageText_Is_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2845,6 +2945,8 @@ public void Should_Throw_If_MessageText_Is_Empty() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2900,6 +3002,8 @@ public void Should_Throw_If_MessageText_Is_WhiteSpace() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -2955,6 +3059,8 @@ public void Should_Set_MessageText(string messageText) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3016,6 +3122,8 @@ public void Should_Set_MessageHtml(string messageHtml) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3077,6 +3185,8 @@ public void Should_Set_MessageHtml(string messageMarkdown) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3140,6 +3250,8 @@ public void Should_Set_Priority(int? priority) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3198,6 +3310,8 @@ public void Should_Handle_PriorityNames_Which_Are_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3253,6 +3367,8 @@ public void Should_Handle_PriorityNames_Which_Are_Empty() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3308,6 +3424,8 @@ public void Should_Handle_PriorityNames_Which_Are_WhiteSpace() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3363,6 +3481,8 @@ public void Should_Set_Priority_Name(string priorityName) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3423,6 +3543,8 @@ public void Should_Set_RuleId(string ruleId) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3483,6 +3605,8 @@ public void Should_Set_RuleName(string ruleName) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3541,6 +3665,8 @@ public void Should_Set_Rule_Url() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3596,6 +3722,8 @@ public void Should_Set_Rule_Url_If_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3656,6 +3784,8 @@ public void Should_Set_Run(string run) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3714,6 +3844,8 @@ public void Should_Throw_If_Provider_Type_Is_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3769,6 +3901,8 @@ public void Should_Throw_If_Provider_Type_Is_Empty() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3824,6 +3958,8 @@ public void Should_Throw_If_Provider_Type_Is_WhiteSpace() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3879,6 +4015,8 @@ public void Should_Set_ProviderType(string providerType) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3937,6 +4075,8 @@ public void Should_Throw_If_Provider_Name_Is_Null() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -3992,6 +4132,8 @@ public void Should_Throw_If_Provider_Name_Is_Empty() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -4047,6 +4189,8 @@ public void Should_Throw_If_Provider_Name_Is_WhiteSpace() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -4102,6 +4246,8 @@ public void Should_Set_ProviderName(string providerName) endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -4170,6 +4316,8 @@ public void Should_Set_AdditionalInformation() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, @@ -4225,6 +4373,8 @@ public void Should_Set_AdditionalInformation_To_Empty_Dictionary() endLine, column, endColumn, + null, // offset + null, // endOffset fileLink, messageText, messageHtml, diff --git a/src/Cake.Issues.Tests/LocationCoordinateConverterTests.cs b/src/Cake.Issues.Tests/LocationCoordinateConverterTests.cs new file mode 100644 index 000000000..9ac8cbf33 --- /dev/null +++ b/src/Cake.Issues.Tests/LocationCoordinateConverterTests.cs @@ -0,0 +1,210 @@ +namespace Cake.Issues.Tests; + +using System; +using System.Collections.Generic; +using Shouldly; +using Xunit; + +public sealed class LocationCoordinateConverterTests +{ + public sealed class TheLineColumnToOffsetMethod + { + [Fact] + public void Should_Return_Null_For_Null_Content() + { + // Given + string content = null; + int line = 1; + int column = 1; + + // When + var result = LocationCoordinateConverter.LineColumnToOffset(content, line, column); + + // Then + result.ShouldBeNull(); + } + + [Fact] + public void Should_Return_Null_For_Empty_Content() + { + // Given + var content = string.Empty; + int line = 1; + int column = 1; + + // When + var result = LocationCoordinateConverter.LineColumnToOffset(content, line, column); + + // Then + result.ShouldBeNull(); + } + + [Fact] + public void Should_Return_Null_For_Invalid_Line() + { + // Given + var content = "line1\nline2\nline3"; + int? line = null; + int column = 1; + + // When + var result = LocationCoordinateConverter.LineColumnToOffset(content, line, column); + + // Then + result.ShouldBeNull(); + } + + [Fact] + public void Should_Return_Zero_For_First_Line_First_Column() + { + // Given + var content = "Hello World"; + int line = 1; + int column = 1; + + // When + var result = LocationCoordinateConverter.LineColumnToOffset(content, line, column); + + // Then + result.ShouldBe(0); + } + + [Fact] + public void Should_Calculate_Offset_For_Single_Line() + { + // Given + var content = "Hello World"; + int line = 1; + int column = 7; // 'W' in "World" + + // When + var result = LocationCoordinateConverter.LineColumnToOffset(content, line, column); + + // Then + result.ShouldBe(6); + } + + [Fact] + public void Should_Calculate_Offset_For_Multiple_Lines() + { + // Given + var content = "line1\nline2\nline3"; + int line = 2; + int column = 3; // 'n' in "line2" + + // When + var result = LocationCoordinateConverter.LineColumnToOffset(content, line, column); + + // Then + result.ShouldBe(8); // 6 chars in "line1\n" + 2 for "li" + } + + [Fact] + public void Should_Handle_Windows_Line_Endings() + { + // Given + var content = "line1\r\nline2\r\nline3"; + int line = 2; + int column = 1; + + // When + var result = LocationCoordinateConverter.LineColumnToOffset(content, line, column); + + // Then + result.ShouldBe(7); // "line1\r\n" = 5 + 2 = 7 + } + } + + public sealed class TheOffsetToLineColumnMethod + { + [Fact] + public void Should_Return_Null_For_Null_Content() + { + // Given + string content = null; + int offset = 5; + + // When + var result = LocationCoordinateConverter.OffsetToLineColumn(content, offset); + + // Then + result.ShouldBeNull(); + } + + [Fact] + public void Should_Return_Null_For_Empty_Content() + { + // Given + var content = string.Empty; + int offset = 5; + + // When + var result = LocationCoordinateConverter.OffsetToLineColumn(content, offset); + + // Then + result.ShouldBeNull(); + } + + [Fact] + public void Should_Return_Null_For_Invalid_Offset() + { + // Given + var content = "Hello World"; + int? offset = null; + + // When + var result = LocationCoordinateConverter.OffsetToLineColumn(content, offset); + + // Then + result.ShouldBeNull(); + } + + [Fact] + public void Should_Return_First_Line_First_Column_For_Zero_Offset() + { + // Given + var content = "Hello World"; + int offset = 0; + + // When + var result = LocationCoordinateConverter.OffsetToLineColumn(content, offset); + + // Then + result.ShouldNotBeNull(); + result.Value.Line.ShouldBe(1); + result.Value.Column.ShouldBe(1); + } + + [Fact] + public void Should_Calculate_Line_Column_For_Single_Line() + { + // Given + var content = "Hello World"; + int offset = 6; // 'W' in "World" + + // When + var result = LocationCoordinateConverter.OffsetToLineColumn(content, offset); + + // Then + result.ShouldNotBeNull(); + result.Value.Line.ShouldBe(1); + result.Value.Column.ShouldBe(7); + } + + [Fact] + public void Should_Calculate_Line_Column_For_Multiple_Lines() + { + // Given + var content = "line1\nline2\nline3"; + int offset = 8; // 'n' in "line2" + + // When + var result = LocationCoordinateConverter.OffsetToLineColumn(content, offset); + + // Then + result.ShouldNotBeNull(); + result.Value.Line.ShouldBe(2); + result.Value.Column.ShouldBe(3); + } + } +} \ No newline at end of file diff --git a/src/Cake.Issues/IIssue.cs b/src/Cake.Issues/IIssue.cs index e4e79b683..a8806e459 100644 --- a/src/Cake.Issues/IIssue.cs +++ b/src/Cake.Issues/IIssue.cs @@ -59,6 +59,18 @@ public interface IIssue /// int? EndColumn { get; } + /// + /// Gets the character offset in the file where the issue has occurred. + /// null if the issue affects the whole file or an assembly. + /// + int? Offset { get; } + + /// + /// Gets the end of the character offset range in the file where the issue has occurred. + /// null if the issue affects the whole file, an assembly or only a single character. + /// + int? EndOffset { get; } + /// /// Gets or sets a link to the position in the file where the issue occurred. /// null if was not set while reading issue. diff --git a/src/Cake.Issues/IIssueProperty.cs b/src/Cake.Issues/IIssueProperty.cs index eecf7b161..e7a4e2245 100644 --- a/src/Cake.Issues/IIssueProperty.cs +++ b/src/Cake.Issues/IIssueProperty.cs @@ -53,68 +53,78 @@ public enum IIssueProperty /// EndColumn = 128, + /// + /// property. + /// + Offset = 256, + + /// + /// property. + /// + EndOffset = 512, + /// /// property. /// - FileLink = 256, + FileLink = 1024, /// /// property. /// - MessageText = 512, + MessageText = 2048, /// /// property. /// - MessageHtml = 1024, + MessageHtml = 4096, /// /// property. /// - MessageMarkdown = 2048, + MessageMarkdown = 8192, /// /// property. /// - Priority = 4096, + Priority = 16384, /// /// property. /// - PriorityName = 8192, + PriorityName = 32768, /// /// property. /// - RuleId = 16384, + RuleId = 65536, /// /// property. /// - RuleName = 32768, + RuleName = 131072, /// /// property. /// - RuleUrl = 65536, + RuleUrl = 262144, /// /// property. /// - Run = 131072, + Run = 524288, /// /// property. /// - ProviderType = 262144, + ProviderType = 1048576, /// /// property. /// - ProviderName = 524288, + ProviderName = 2097152, /// /// property. /// - AdditionalInformation = 1048576, + AdditionalInformation = 4194304, } diff --git a/src/Cake.Issues/Issue.cs b/src/Cake.Issues/Issue.cs index 1b15e3dd8..ed5a4b7eb 100644 --- a/src/Cake.Issues/Issue.cs +++ b/src/Cake.Issues/Issue.cs @@ -30,6 +30,10 @@ public class Issue : IIssue /// null if the issue affects the whole file or an assembly. /// The end of the column range in the file where the issues have occurred. /// null if the issue affects the whole file, an assembly or only a single column. + /// The character offset in the file where the issue has occurred. + /// null if the issue affects the whole file or an assembly. + /// The end of the character offset range in the file where the issue has occurred. + /// null if the issue affects the whole file, an assembly or only a single character. /// Link to the position in the file where the issue occurred. /// null if no link is available. /// The message of the issue in plain text format. @@ -58,6 +62,8 @@ public Issue( int? endLine, int? column, int? endColumn, + int? offset, + int? endOffset, Uri fileLink, string messageText, string messageHtml, @@ -77,6 +83,8 @@ public Issue( endLine?.NotNegativeOrZero(); column?.NotNegativeOrZero(); endColumn?.NotNegativeOrZero(); + offset?.NotNegativeOrZero(); + endOffset?.NotNegativeOrZero(); messageText.NotNullOrWhiteSpace(); providerType.NotNullOrWhiteSpace(); providerName.NotNullOrWhiteSpace(); @@ -148,12 +156,30 @@ public Issue( throw new ArgumentOutOfRangeException(nameof(endColumn), "Column range needs to end after start of range."); } + // Offset validation + if (this.AffectedFileRelativePath == null && offset.HasValue) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Cannot specify an offset while not specifying a file."); + } + + if (!offset.HasValue && endOffset.HasValue) + { + throw new ArgumentOutOfRangeException(nameof(endOffset), "Cannot specify the end of offset range while not specifying start of offset range."); + } + + if (offset.HasValue && endOffset.HasValue && offset.Value > endOffset.Value) + { + throw new ArgumentOutOfRangeException(nameof(endOffset), "Offset range needs to end after start of range."); + } + this.Identifier = identifier; this.ProjectName = projectName; this.Line = line; this.EndLine = endLine; this.Column = column; this.EndColumn = endColumn; + this.Offset = offset; + this.EndOffset = endOffset; this.FileLink = fileLink; this.MessageText = messageText; this.MessageHtml = messageHtml; @@ -193,6 +219,12 @@ public Issue( /// public int? EndColumn { get; } + /// + public int? Offset { get; } + + /// + public int? EndOffset { get; } + /// public Uri FileLink { get; set; } diff --git a/src/Cake.Issues/IssueBuilder.cs b/src/Cake.Issues/IssueBuilder.cs index 31937c3ae..1a2cb11d8 100644 --- a/src/Cake.Issues/IssueBuilder.cs +++ b/src/Cake.Issues/IssueBuilder.cs @@ -21,6 +21,8 @@ public class IssueBuilder private int? endLine; private int? column; private int? endColumn; + private int? offset; + private int? endOffset; private Uri fileLink; private int? priority; private string priorityName; @@ -293,6 +295,48 @@ public IssueBuilder InFile(string filePath, int? startLine, int? endLine, int? s return this; } + /// + /// Sets the path to the file affected by the issue and the character offset where the issue has occurred. + /// + /// The path to the file affected by the issue. + /// The path needs to be relative to the repository root. + /// null or if issue is not related to a change in a file. + /// The character offset in the file where the issue has occurred. + /// null if the issue affects the whole file or an assembly. + /// Issue Builder instance. + public IssueBuilder InFileAtOffset(string filePath, int? offset) + { + offset?.NotNegativeOrZero(); + + this.filePath = filePath; + this.offset = offset; + + return this; + } + + /// + /// Sets the path to the file affected by the issue and the character offset range where the issue has occurred. + /// + /// The path to the file affected by the issue. + /// The path needs to be relative to the repository root. + /// null or if issue is not related to a change in a file. + /// The character offset in the file where the issue has occurred. + /// null if the issue affects the whole file or an assembly. + /// The end of the character offset range in the file where the issue has occurred. + /// null if the issue affects the whole file, an assembly or only a single character. + /// Issue Builder instance. + public IssueBuilder InFileAtOffset(string filePath, int? startOffset, int? endOffset) + { + startOffset?.NotNegativeOrZero(); + endOffset?.NotNegativeOrZero(); + + this.filePath = filePath; + this.offset = startOffset; + this.endOffset = endOffset; + + return this; + } + /// /// Sets additional information regarding the issue. /// @@ -478,6 +522,8 @@ private Issue CreateIssue(Uri fileLink) => this.endLine, this.column, this.endColumn, + this.offset, + this.endOffset, fileLink, this.messageText, this.messageHtml, diff --git a/src/Cake.Issues/IssueProviderSettings.cs b/src/Cake.Issues/IssueProviderSettings.cs index 591c499e9..bffb3f460 100644 --- a/src/Cake.Issues/IssueProviderSettings.cs +++ b/src/Cake.Issues/IssueProviderSettings.cs @@ -18,6 +18,7 @@ public IssueProviderSettings(FilePath logFilePath) logFilePath.NotNull(); this.LogFileContent = File.ReadAllBytes(logFilePath.FullPath); + this.PreferredLocationCoordinates = LocationCoordinates.LineColumn; } /// @@ -30,10 +31,16 @@ public IssueProviderSettings(byte[] logFileContent) logFileContent.NotNull(); this.LogFileContent = logFileContent; + this.PreferredLocationCoordinates = LocationCoordinates.LineColumn; } /// /// Gets the content of the log file. /// public byte[] LogFileContent { get; } + + /// + /// Gets or sets the preferred coordinate format for issue locations. + /// + public LocationCoordinates PreferredLocationCoordinates { get; set; } } diff --git a/src/Cake.Issues/LocationCoordinateConverter.cs b/src/Cake.Issues/LocationCoordinateConverter.cs new file mode 100644 index 000000000..6a1fe4bef --- /dev/null +++ b/src/Cake.Issues/LocationCoordinateConverter.cs @@ -0,0 +1,183 @@ +namespace Cake.Issues; + +using System; +using System.IO; +using System.Text; + +/// +/// Utility methods for converting between different location coordinate formats. +/// +public static class LocationCoordinateConverter +{ + /// + /// Converts line and column to character offset. + /// + /// The content of the file. + /// The line number (1-based). + /// The column number (1-based). If null, returns offset to start of line. + /// The character offset (0-based) or null if conversion fails. + public static int? LineColumnToOffset(string fileContent, int? line, int? column = null) + { + if (string.IsNullOrEmpty(fileContent) || !line.HasValue || line.Value < 1) + { + return null; + } + + try + { + var currentLine = 1; + + for (var i = 0; i < fileContent.Length; i++) + { + if (currentLine == line.Value) + { + if (!column.HasValue || column.Value < 1) + { + return i; // Return current position as offset + } + + // Count characters until we reach the desired column or end of line + var currentColumn = 1; + var startOffset = i; + while (i < fileContent.Length && currentColumn < column.Value && + fileContent[i] != '\r' && fileContent[i] != '\n') + { + i++; + currentColumn++; + } + + return startOffset + (currentColumn - 1); + } + + if (fileContent[i] == '\r') + { + // Handle Windows line ending (\r\n) + if (i + 1 < fileContent.Length && fileContent[i + 1] == '\n') + { + i++; // Skip the \n + } + currentLine++; + } + else if (fileContent[i] == '\n') + { + // Handle Unix line ending (\n) + currentLine++; + } + } + } + catch + { + // Return null on any error + } + + return null; + } + + /// + /// Converts line and column to character offset using byte array content. + /// + /// The content of the file as byte array. + /// The line number (1-based). + /// The column number (1-based). If null, returns offset to start of line. + /// The encoding to use. If null, UTF-8 is used. + /// The character offset (0-based) or null if conversion fails. + public static int? LineColumnToOffset(byte[] fileContent, int? line, int? column = null, Encoding encoding = null) + { + if (fileContent == null || fileContent.Length == 0) + { + return null; + } + + try + { + var content = (encoding ?? Encoding.UTF8).GetString(fileContent); + return LineColumnToOffset(content, line, column); + } + catch + { + return null; + } + } + + /// + /// Converts character offset to line and column. + /// + /// The content of the file. + /// The character offset (0-based). + /// A tuple containing line (1-based) and column (1-based), or null if conversion fails. + public static (int Line, int Column)? OffsetToLineColumn(string fileContent, int? offset) + { + if (string.IsNullOrEmpty(fileContent) || !offset.HasValue || offset.Value < 0) + { + return null; + } + + try + { + var line = 1; + var column = 1; + var currentOffset = 0; + + for (var i = 0; i < Math.Min(offset.Value, fileContent.Length); i++) + { + if (fileContent[i] == '\r') + { + // Handle Windows line ending (\r\n) + if (i + 1 < fileContent.Length && fileContent[i + 1] == '\n') + { + if (currentOffset == offset.Value) + { + return (line, column); + } + i++; // Skip the \n + } + line++; + column = 1; + } + else if (fileContent[i] == '\n') + { + // Handle Unix line ending (\n) + line++; + column = 1; + } + else + { + column++; + } + + currentOffset++; + } + + return (line, column); + } + catch + { + return null; + } + } + + /// + /// Converts character offset to line and column using byte array content. + /// + /// The content of the file as byte array. + /// The character offset (0-based). + /// The encoding to use. If null, UTF-8 is used. + /// A tuple containing line (1-based) and column (1-based), or null if conversion fails. + public static (int Line, int Column)? OffsetToLineColumn(byte[] fileContent, int? offset, Encoding encoding = null) + { + if (fileContent == null || fileContent.Length == 0) + { + return null; + } + + try + { + var content = (encoding ?? Encoding.UTF8).GetString(fileContent); + return OffsetToLineColumn(content, offset); + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/src/Cake.Issues/LocationCoordinates.cs b/src/Cake.Issues/LocationCoordinates.cs new file mode 100644 index 000000000..55e7cbb51 --- /dev/null +++ b/src/Cake.Issues/LocationCoordinates.cs @@ -0,0 +1,22 @@ +namespace Cake.Issues; + +/// +/// Specifies the preferred coordinate format for issue locations. +/// +public enum LocationCoordinates +{ + /// + /// Use line and column coordinates. + /// + LineColumn, + + /// + /// Use character offset coordinates. + /// + Offset, + + /// + /// Use both line/column and offset coordinates when available. + /// + Both, +} \ No newline at end of file diff --git a/src/Cake.Issues/Serialization/SerializableIssueExtensions.cs b/src/Cake.Issues/Serialization/SerializableIssueExtensions.cs index a8a589e07..bd93d4c4a 100644 --- a/src/Cake.Issues/Serialization/SerializableIssueExtensions.cs +++ b/src/Cake.Issues/Serialization/SerializableIssueExtensions.cs @@ -32,6 +32,8 @@ internal static Issue ToIssue(this SerializableIssue serializableIssue) null, null, null, + null, // offset + null, // endOffset null, serializableIssue.Message, null, diff --git a/src/Cake.Issues/Serialization/SerializableIssueV2Extensions.cs b/src/Cake.Issues/Serialization/SerializableIssueV2Extensions.cs index c925128b0..9c4f7e7d4 100644 --- a/src/Cake.Issues/Serialization/SerializableIssueV2Extensions.cs +++ b/src/Cake.Issues/Serialization/SerializableIssueV2Extensions.cs @@ -32,6 +32,8 @@ internal static Issue ToIssue(this SerializableIssueV2 serializableIssue) null, null, null, + null, // offset + null, // endOffset null, serializableIssue.MessageText, serializableIssue.MessageHtml, diff --git a/src/Cake.Issues/Serialization/SerializableIssueV3Extensions.cs b/src/Cake.Issues/Serialization/SerializableIssueV3Extensions.cs index 59fb2ef67..c8ccfb1da 100644 --- a/src/Cake.Issues/Serialization/SerializableIssueV3Extensions.cs +++ b/src/Cake.Issues/Serialization/SerializableIssueV3Extensions.cs @@ -38,6 +38,8 @@ internal static Issue ToIssue(this SerializableIssueV3 serializableIssue) serializableIssue.EndLine, serializableIssue.Column, serializableIssue.EndColumn, + null, // offset + null, // endOffset fileLink, serializableIssue.MessageText, serializableIssue.MessageHtml, diff --git a/src/Cake.Issues/Serialization/SerializableIssueV4Extensions.cs b/src/Cake.Issues/Serialization/SerializableIssueV4Extensions.cs index 4534196d5..4de7653ca 100644 --- a/src/Cake.Issues/Serialization/SerializableIssueV4Extensions.cs +++ b/src/Cake.Issues/Serialization/SerializableIssueV4Extensions.cs @@ -37,6 +37,8 @@ internal static Issue ToIssue(this SerializableIssueV4 serializableIssue) serializableIssue.EndLine, serializableIssue.Column, serializableIssue.EndColumn, + null, // offset + null, // endOffset fileLink, serializableIssue.MessageText, serializableIssue.MessageHtml, diff --git a/src/Cake.Issues/Serialization/SerializableIssueV5Extensions.cs b/src/Cake.Issues/Serialization/SerializableIssueV5Extensions.cs index 8c39ec9b2..487e57048 100644 --- a/src/Cake.Issues/Serialization/SerializableIssueV5Extensions.cs +++ b/src/Cake.Issues/Serialization/SerializableIssueV5Extensions.cs @@ -37,6 +37,8 @@ internal static Issue ToIssue(this SerializableIssueV5 serializableIssue) serializableIssue.EndLine, serializableIssue.Column, serializableIssue.EndColumn, + null, // offset + null, // endOffset fileLink, serializableIssue.MessageText, serializableIssue.MessageHtml,