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,